changeset 0:15c1ee57764c draft

Uploaded
author insilico-bob
date Thu, 07 Apr 2016 14:43:37 -0400
parents
children ff5ed08cb01e
files ._mda_heatmap_viz mda_heatmap_viz/._.DS_Store mda_heatmap_viz/._mda_heatmap_viz.xml mda_heatmap_viz/config/._.DS_Store mda_heatmap_viz/mda_heatmap_viz.xml mda_heatmap_viz/static/._.DS_Store mda_heatmap_viz/static/css/NGCHM.css mda_heatmap_viz/static/images/addButton.png mda_heatmap_viz/static/images/breakButtonOff.png mda_heatmap_viz/static/images/breakButtonOn.png mda_heatmap_viz/static/images/cancel.png mda_heatmap_viz/static/images/classButtonOff.png mda_heatmap_viz/static/images/classButtonOn.png mda_heatmap_viz/static/images/closeButton.png mda_heatmap_viz/static/images/covariateBarsOff.png mda_heatmap_viz/static/images/covariateBarsOn.png mda_heatmap_viz/static/images/createPdf.png mda_heatmap_viz/static/images/dataLayersOff.png mda_heatmap_viz/static/images/dataLayersOn.png mda_heatmap_viz/static/images/filterClassButton.png mda_heatmap_viz/static/images/full.png mda_heatmap_viz/static/images/full_selected.png mda_heatmap_viz/static/images/gear.png mda_heatmap_viz/static/images/gearDis.png mda_heatmap_viz/static/images/gear_big.png mda_heatmap_viz/static/images/go.png mda_heatmap_viz/static/images/join.png mda_heatmap_viz/static/images/mdandersonlogo260x85.png mda_heatmap_viz/static/images/minusButton.png mda_heatmap_viz/static/images/next.png mda_heatmap_viz/static/images/pdf.png mda_heatmap_viz/static/images/plusButton.png mda_heatmap_viz/static/images/prefApply.png mda_heatmap_viz/static/images/prefBack.png mda_heatmap_viz/static/images/prefCancel.png mda_heatmap_viz/static/images/prefSave.png mda_heatmap_viz/static/images/prev.png mda_heatmap_viz/static/images/removeFilterClassButton.png mda_heatmap_viz/static/images/ribbonH.png mda_heatmap_viz/static/images/ribbonH_selected.png mda_heatmap_viz/static/images/ribbonV.png mda_heatmap_viz/static/images/ribbonV_selected.png mda_heatmap_viz/static/images/rowsColsOff.png mda_heatmap_viz/static/images/rowsColsOn.png mda_heatmap_viz/static/images/save.png mda_heatmap_viz/static/images/searchButton.png mda_heatmap_viz/static/images/split.png mda_heatmap_viz/static/images/zoom-in.png mda_heatmap_viz/static/images/zoom-out.png mda_heatmap_viz/static/javascript/._UserPreferenceManager.js.sav mda_heatmap_viz/static/javascript/.gitignore mda_heatmap_viz/static/javascript/ColorMapManager.js mda_heatmap_viz/static/javascript/DetailHeatMapDisplay.js mda_heatmap_viz/static/javascript/MatrixManager.js mda_heatmap_viz/static/javascript/NGCHM_Util.js mda_heatmap_viz/static/javascript/PdfGenerator.js mda_heatmap_viz/static/javascript/SelectionManager.js mda_heatmap_viz/static/javascript/SummaryHeatMapDisplay.js mda_heatmap_viz/static/javascript/UserHelpManager.js mda_heatmap_viz/static/javascript/UserPreferenceManager.js mda_heatmap_viz/static/javascript/UserPreferenceManager.js.sav mda_heatmap_viz/static/javascript/custom.js mda_heatmap_viz/static/javascript/lib/deflate.js mda_heatmap_viz/static/javascript/lib/inflate.js mda_heatmap_viz/static/javascript/lib/jspdf.debug.js mda_heatmap_viz/static/javascript/lib/z-worker.js mda_heatmap_viz/static/javascript/lib/zip.js mda_heatmap_viz/templates/._mda_heatmap_viz.mako mda_heatmap_viz/templates/MDAheatmap.oldmako mda_heatmap_viz/templates/mda_heatmap_viz.mako
diffstat 70 files changed, 22459 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
Binary file ._mda_heatmap_viz has changed
Binary file mda_heatmap_viz/._.DS_Store has changed
Binary file mda_heatmap_viz/._mda_heatmap_viz.xml has changed
Binary file mda_heatmap_viz/config/._.DS_Store has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mda_heatmap_viz/mda_heatmap_viz.xml	Thu Apr 07 14:43:37 2016 -0400
@@ -0,0 +1,16 @@
+<?xml version="1.0"?>
+<tool_dependency>
+ <package name="mda_heatmap_viz" version="1.0.0">
+        <!-- <repository name="zzzngchm" owner="zzzinsilico-bob" toolshed="zzz toolshed.g2.bx.psu.edu"  />
+        <install>1:4617b66763af</install> -->
+  <install version="1.0">
+    <actions_group>
+        <action type="make_directory">$Galaxy_Dir/config/plugins/visualizations/mda_heatmap_viz</action>
+        <action type="move_directory_files">
+            <source_directory>$__tool_directory__/mda_heatmap_viz</source_directory>
+            <destination_directory>$Galaxy_Dir/config/plugins/visualizations/mda_heatmap_viz</destination_directory>
+        </action>
+    </actions_group>
+  </install>
+ </package>
+</tool_dependency>
Binary file mda_heatmap_viz/static/._.DS_Store has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mda_heatmap_viz/static/css/NGCHM.css	Thu Apr 07 14:43:37 2016 -0400
@@ -0,0 +1,291 @@
+@CHARSET "ISO-8859-1";
+/**
+ *  */
+
+
+body, html {
+    width:100%;
+    height:100%;
+    margin:0;
+    padding:0;
+    overflow:auto;
+    font-family: sans-serif;
+    font-family: arial;  
+    font-size: 15px;
+}
+
+div.mdaServiceHeader
+{
+	float: none;
+	width: 100%;
+	height: 40px;
+	color: #666;
+	margin: 2px 0px 2px 0px;
+	background-color: white;
+	border-bottom: 4px solid #da0505;
+	z-index: -1;
+	overflow: hidden;
+}
+
+div.mdaServiceHeaderLogo
+{
+	float: left;
+	width: auto;
+}
+
+div.mdaServiceHeaderLogo img
+{
+	height: 35px;
+}
+
+
+#divider{
+	height: 82%;
+	width: 5px;
+	vertical-align:top;
+	display:inline-block;
+	background: #666666;
+	cursor: ew-resize;
+}
+
+#summary_chm {
+    height:100%;
+    width:48%;
+    vertical-align:top;
+    margin-left: 3px;
+    display:inline-block;
+}
+
+#summary_canvas {
+    height:82%;
+    width:98%;
+	zIndex:1;
+}
+
+#classBarLabels{
+	height:95%;
+	width:95%;
+	pointer-events:none;
+}
+
+#detail_chm {
+    height:100%;
+    width:48%;
+    vertical-align:top;
+    display:inline-block;
+}
+
+#detail_canvas {
+    height:82%;
+    width:90%;
+	zIndex:1;
+}
+
+#detail_buttons {
+	margin-top: 10px;
+}
+
+#chmFile {
+	display: none
+}
+
+#helptext {
+    margin: 5px !important;
+    padding: 10px 10px 10px 10px !important;
+    border: 1px solid #1a1a1a;
+    border-radius: 15px;
+    font-size: 90% !important;
+    font-family: Arial;
+    font-weight: normal;
+    background: #dddddd;
+    color: #000000;
+    min-width: 100px;
+    z-index: 10000;
+    text-align: center;
+    box-shadow: 5px 5px 5px #777777;
+}
+
+#helpprefs {
+    margin: 5px !important;
+    padding: 8px 8px 8px 8px !important;
+    border: 1px solid #1a1a1a;
+    border-radius: 15px;
+    font-size: 90% !important;
+    font-family: Arial;
+    font-weight: normal;
+    background: #dddddd;
+    color: #000000;
+    z-index: 10000;
+    text-align: center;
+    height: auto;
+    width: auto;
+    box-shadow: 5px 5px 5px #777777;
+    white-space: nowrap;
+}
+
+#prefprefs {
+	position: relative;
+    display: inline-block;
+}
+
+
+.input-color {
+    position: relative;
+}
+.input-color input {
+    padding-left: 0px;
+}
+.input-color .color-box {
+    width: 30px;
+    height: 15px;
+    display: inline-block;
+    background-color: #ccc;
+    position: absolute;
+    left: -8px;
+    top: -8px;
+}
+
+table {
+    width: 100%;
+}
+
+td {
+    height: 0px;
+	padding: 0px;
+    font-size: 80% !important;
+    font-family: Arial;
+    font-weight: normal;
+    color: #0843c1;
+    vertical-align: center;
+}
+
+.searchItem{
+	background-color: yellow;
+}
+
+#searchError{
+	border: 1px solid #1a1a1a;
+    border-radius: 10px;
+    font-size: 90% !important;
+    font-family: Arial;
+    font-weight: normal;
+    background: #dddddd;
+    color: red;
+    position: absolute;
+    width: 180px;
+    z-index:10;
+    /* height: 50px; */
+    text-align: center;
+    box-shadow: 5px 5px 5px #777777;
+}
+
+.labelMenu{
+    border: 1px solid #1a1a1a;
+    border-radius: 10px;
+    font-size: 90% !important;
+    font-family: Arial;
+    font-weight: normal;
+    background: #dddddd;
+    color: #000000;
+    width: 240px;
+    height: 150px;
+    text-align: center;
+    box-shadow: 5px 5px 5px #777777;
+    position:relative;
+}
+
+.labelMenuCaption{
+	padding: .4em 1em;
+	background: #0843c1;
+	color: #ffffff;
+	font-size: 90% !important;
+    font-family: Arial;
+    font-weight: bold;
+    text-align: left;
+	border-top-left-radius: 10px;
+    border-top-right-radius: 10px;
+}
+
+.labelMenuClose{
+	position: absolute;
+	right: 2px;
+	bottom:2px;
+}
+
+.labelMenuHeader tr{
+	padding: 4em 1em;
+	position:relative;
+	margin: 0px 0px 0px 0px;
+	background: #f8f3f1; 
+	color: #0843c1;
+	text-align: left;
+	font-size: 90% !important;
+    font-family: Arial;
+    font-weight: normal;
+	border-radius: 10px;
+}
+
+.labelMenuBody tr{
+	height: 20px;
+}
+.labelMenuBody td{
+	height:20px;
+	position: absolute;
+	font-size: 80% !important;
+    font-family: Arial;
+    font-weight: normal;
+    color: #0843c1;
+    cursor: pointer;
+}
+
+.labelMenuBody td:hover{
+	font-weight: bold;
+}
+
+table.breakpointContainer {
+    border: none;
+    border-spacing: 0px;
+    border-collapse: collapse;
+}
+
+td.breakpointContainer
+th.breakpointContainer {
+    text-align: center;
+    margin: 0px;
+    padding: 0px 5px 0px 0px;
+    border-spacing: 0px;
+    border-collapse: collapse;    
+}
+
+#prefsPanel, #pdfPrefsPanel {
+    margin: 5px !important;
+    padding: 2px 2px 2px 2px !important;
+    border: 1px solid #1a1a1a;
+    border-radius: 15px;
+    font-size: 90% !important;
+    font-family: Arial;
+    font-weight: normal;
+    background: #dddddd;
+    color: #000000;
+    z-index: 10000;
+    text-align: center;
+    height: auto;
+    width: auto;
+    box-shadow: 5px 5px 5px #777777;
+    white-space: nowrap;
+}
+#prefsHeader, #pdfPrefsHeader{
+    display: table;
+    font-size: 18px;
+    font-weight: bold;
+    color: #0843c1;
+    margin: auto;
+    width: 100%;
+    height: 30px;
+    line-height: 30px;
+    text-align: center;
+    border-top-left-radius: 15px;
+    border-top-right-radius: 15px;
+    background: #CBD1EA;
+}
+
Binary file mda_heatmap_viz/static/images/addButton.png has changed
Binary file mda_heatmap_viz/static/images/breakButtonOff.png has changed
Binary file mda_heatmap_viz/static/images/breakButtonOn.png has changed
Binary file mda_heatmap_viz/static/images/cancel.png has changed
Binary file mda_heatmap_viz/static/images/classButtonOff.png has changed
Binary file mda_heatmap_viz/static/images/classButtonOn.png has changed
Binary file mda_heatmap_viz/static/images/closeButton.png has changed
Binary file mda_heatmap_viz/static/images/covariateBarsOff.png has changed
Binary file mda_heatmap_viz/static/images/covariateBarsOn.png has changed
Binary file mda_heatmap_viz/static/images/createPdf.png has changed
Binary file mda_heatmap_viz/static/images/dataLayersOff.png has changed
Binary file mda_heatmap_viz/static/images/dataLayersOn.png has changed
Binary file mda_heatmap_viz/static/images/filterClassButton.png has changed
Binary file mda_heatmap_viz/static/images/full.png has changed
Binary file mda_heatmap_viz/static/images/full_selected.png has changed
Binary file mda_heatmap_viz/static/images/gear.png has changed
Binary file mda_heatmap_viz/static/images/gearDis.png has changed
Binary file mda_heatmap_viz/static/images/gear_big.png has changed
Binary file mda_heatmap_viz/static/images/go.png has changed
Binary file mda_heatmap_viz/static/images/join.png has changed
Binary file mda_heatmap_viz/static/images/mdandersonlogo260x85.png has changed
Binary file mda_heatmap_viz/static/images/minusButton.png has changed
Binary file mda_heatmap_viz/static/images/next.png has changed
Binary file mda_heatmap_viz/static/images/pdf.png has changed
Binary file mda_heatmap_viz/static/images/plusButton.png has changed
Binary file mda_heatmap_viz/static/images/prefApply.png has changed
Binary file mda_heatmap_viz/static/images/prefBack.png has changed
Binary file mda_heatmap_viz/static/images/prefCancel.png has changed
Binary file mda_heatmap_viz/static/images/prefSave.png has changed
Binary file mda_heatmap_viz/static/images/prev.png has changed
Binary file mda_heatmap_viz/static/images/removeFilterClassButton.png has changed
Binary file mda_heatmap_viz/static/images/ribbonH.png has changed
Binary file mda_heatmap_viz/static/images/ribbonH_selected.png has changed
Binary file mda_heatmap_viz/static/images/ribbonV.png has changed
Binary file mda_heatmap_viz/static/images/ribbonV_selected.png has changed
Binary file mda_heatmap_viz/static/images/rowsColsOff.png has changed
Binary file mda_heatmap_viz/static/images/rowsColsOn.png has changed
Binary file mda_heatmap_viz/static/images/save.png has changed
Binary file mda_heatmap_viz/static/images/searchButton.png has changed
Binary file mda_heatmap_viz/static/images/split.png has changed
Binary file mda_heatmap_viz/static/images/zoom-in.png has changed
Binary file mda_heatmap_viz/static/images/zoom-out.png has changed
Binary file mda_heatmap_viz/static/javascript/._UserPreferenceManager.js.sav has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mda_heatmap_viz/static/javascript/.gitignore	Thu Apr 07 14:43:37 2016 -0400
@@ -0,0 +1,1 @@
+/temp.js
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mda_heatmap_viz/static/javascript/ColorMapManager.js	Thu Apr 07 14:43:37 2016 -0400
@@ -0,0 +1,246 @@
+
+function ColorMap(colorMapObj){
+	var type = colorMapObj["type"];
+	var thresholds;
+	if (type == "quantile"){
+		thresholds = colorMapObj["linearEquiv"];
+	}	else {
+		thresholds = colorMapObj["thresholds"];
+	}
+	var numBreaks = thresholds.length;
+	
+	// Hex colors
+	var colors = colorMapObj["colors"];
+	var missingColor = colorMapObj["missing"];
+	
+	// RGBA colors
+	var rgbaColors = [];
+	var rgbaMissingColor;
+	
+	if (colorMapObj["rgbaColors"] != undefined){
+		rgbaColors = colorMapObj["rgbaColors"];
+	} else {
+		for (var i =0; i<numBreaks; i++){
+			rgbaColors[i] = hexToRgba(colors[i]);
+		}
+	}
+	
+	if (colorMapObj["rgbaMissingColor"] != undefined){
+		rgbaMissingColors = colorMapObj["rgbaMissingColor"];
+	} else {
+		rgbaMissingColor = hexToRgba(missingColor);
+	}
+	
+	this.getThresholds = function(){
+		return thresholds;
+	}
+	
+	this.setThresholds = function(newthresholds){
+		thresholds = newthresholds;
+	}
+	/**********************************************************************************
+	 * FUNCTION - getContinuousThresholdKeys: This function calculates and returns an
+	 * array containing 10 continuous threshold breakpoint keys from the original thresholds 
+	 * submitted.  It is used only for rendering a continuous classification bar help.  
+	 **********************************************************************************/
+	this.getContinuousThresholdKeys = function(){
+    	var conThresh = new Array();
+    	var bottomThresh = thresholds[0];
+    	var threshSize = this.getContinuousThresholdKeySize();
+    	//Add first threshold from original threshold list
+    	conThresh.push(bottomThresh);
+    	//Calculate and create "interim" 8 thresholds
+    	for (var i = 1; i <= 8; i++){
+	    	conThresh.push(bottomThresh+Math.floor(threshSize*i));
+    	}
+    	//Add last threshold from original threshold list
+    	conThresh.push(thresholds[thresholds.length - 1]);  
+    	return conThresh;
+	}
+	
+	/**********************************************************************************
+	 * FUNCTION - getContinuousThresholdKeySize: This function calculates the size 
+	 * separating each "interim" threshold key for a continuous classification bar.  
+	 **********************************************************************************/
+	this.getContinuousThresholdKeySize = function(){
+    	var bottomThresh = thresholds[0];
+    	var topThresh = thresholds[thresholds.length - 1]; 
+    	return (topThresh - bottomThresh) / 8;
+	}
+	
+	this.getColors = function(){
+		return colors;
+	}
+	this.setColors = function(newcolors){
+		colors = newcolors;
+	}
+	this.getType = function(){
+		return type;
+	}
+	this.getMissingColor = function(){
+		return missingColor;
+	}
+	this.setMissingColor = function(color){
+		missingColor = color;
+	}
+	
+	
+	// returns an RGBA value from the given value
+	this.getColor = function(value){
+		var color;
+	
+		if (isNaN(value)){
+			color = rgbaMissingColor;
+		}else if(value < thresholds[0]){
+			color = rgbaColors[0]; // return color for lowest threshold if value is below range
+		} else if (value >= thresholds[numBreaks-1]){
+			color = rgbaColors[numBreaks-1]; // return color for highest threshold if value is above range
+		} else {
+			var bounds = findBounds(value, thresholds);
+			color = blendColors(value, bounds);
+		}
+		
+		return color;
+	}
+	
+	this.getClassificationColor = function(value){
+		var color;
+		if (type == "discrete"){
+			for (var i = 0; i < thresholds.length; i++){
+				if (value == thresholds[i]){
+					color = rgbaColors[i];
+					return color;
+				}
+			}
+			return rgbaMissingColor;
+		} else {
+			if (isNaN(value)){
+				color = rgbaMissingColor;
+			}else{
+				color = this.getColor(value);
+			}
+		}
+		
+		return color;
+	}
+	
+	this.addBreakpoint = function(value,color){
+		var bounds = findBounds(value, thresholds);
+		thresholds.splice(bounds["lower"],0,value);
+		colors.splice(bounds["lower"],0,color);
+		rgbaColors.splice(bounds["lower"],0,hexToRgba(color));
+	}
+	
+	this.changeBreakpoint = function(value,newColor){
+		var bounds = findBounds(value, thresholds);
+		thresholds.splice(bounds["lower"],1,value);
+		colors.splice(bounds["lower"],1,newColor);
+		rgbaColors.splice(bounds["lower"],1,hexToRgba(newColor));
+	}
+	
+	this.removeBreakpoint = function(value){
+		var bounds = findBounds(value, thresholds);
+		thresholds.splice(bounds["lower"],1);
+		colors.splice(bounds["lower"],1);
+		rgbaColors.splice(bounds["lower"],1);
+	}
+	
+	//===========================//
+	// internal helper functions //
+	//===========================//
+	
+	function findBounds(value, thresholds){
+		var bounds = {};
+		var i =0;
+		while (i<numBreaks){
+			if (thresholds[i] <= value && value < thresholds[i+1]){
+				bounds["upper"] = i+1;
+				bounds["lower"] = i;
+				break;
+			}
+			i++;
+		}
+		return bounds;
+	}
+	
+	function blendColors(value, bounds){
+		var ratio = (value - thresholds[bounds["lower"]])/(thresholds[bounds["upper"]]-thresholds[bounds["lower"]]);
+		var lowerColor = rgbaColors[bounds["lower"]];
+		var upperColor = rgbaColors[bounds["upper"]];
+		// lowerColor and upperColor should be in { r:###, g:###, b:### } format
+		var color = {};
+		color["r"] = Math.round(lowerColor["r"] * (1.0 - ratio) + upperColor["r"] * ratio);
+	    color["g"] = Math.round(lowerColor["g"] * (1.0 - ratio) + upperColor["g"] * ratio);
+	    color["b"] = Math.round(lowerColor["b"] * (1.0 - ratio) + upperColor["b"] * ratio);
+	    color["a"] = 255;
+	    return color;
+	}
+	
+	function hexToRgba(hex) { // I didn't write this function. I'm not that clever. Thanks stackoverflow
+	    var rgbColor = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
+	    return rgbColor ? {
+	        r: parseInt(rgbColor[1], 16),
+	        g: parseInt(rgbColor[2], 16),
+	        b: parseInt(rgbColor[3], 16),
+	        a: 255
+	    } : null;
+	}
+
+	this.getHexToRgba = function(hex){
+		return hexToRgba(hex);
+	}
+	
+	this.getRgbToHex = function(rgb) {
+		var a = rgb.a
+		var r = rgb.r
+		var g = rgb.g
+		var b = rgb.b
+	    return ('#' + componentToHex(r) + componentToHex(g) + componentToHex(b));
+	}
+	
+	function componentToHex(c) {
+	    var hex = c.toString(16);
+	    return hex.length == 1 ? "0" + hex : hex;
+	}
+
+}
+		
+// All color maps and current color maps are stored here.
+function ColorMapManager(colorMaps){
+	
+	var colorMapCollection = colorMaps.colormaps;
+	
+	var mainColorMap;
+	var flickColorMap;
+	
+	this.getMainColorMap = function(){
+		return mainColorMap;
+	}
+	
+	this.setMainColorMap = function(colorMapName){
+		mainColorMap = new ColorMap(colorMapCollection[colorMapName]);
+		return mainColorMap;
+	}
+	
+	
+	this.getFlickColorMap = function(){
+		return flickColorMap;
+	}
+	
+	this.setFlickColorMap = function(colorMapName){
+		flickColorMap = new ColorMap(colorMapCollection[colorMapName]);
+		return flickColorMap;
+	}
+	
+	this.getColorMap = function(colorMapName){
+		var colorMap = new ColorMap(colorMapCollection[colorMapName]);
+		return colorMap;
+	}
+	
+	this.setColorMap = function(colorMapName, colorMap){
+		colorMapCollection[colorMapName].colors = colorMap.getColors();
+		colorMapCollection[colorMapName].thresholds = colorMap.getThresholds();
+		colorMapCollection[colorMapName].missing = colorMap.getMissingColor();
+	}
+	
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mda_heatmap_viz/static/javascript/DetailHeatMapDisplay.js	Thu Apr 07 14:43:37 2016 -0400
@@ -0,0 +1,1616 @@
+var detCanvas;
+var det_gl; // WebGL contexts
+var detTextureParams;
+var labelElement; 
+var old_mouse_pos = [0, 0];
+
+
+var detCanvasScaleArray = new Float32Array([1.0, 1.0]);
+var detCanvasBoxLeftTopArray = new Float32Array([0, 0]);
+var detCanvasBoxRightBottomArray = new Float32Array([0, 0]);
+var detCanvasTranslateArray = new Float32Array([0, 0]);
+
+var detTexPixels;
+var detTexPixelsCache;
+
+var detUScale;
+var detUTranslate;
+var detUBoxLeftTop;
+var detUBoxRightBottom;
+var detUBoxThickness;
+var detUBoxColor;
+
+var detEventTimer = 0; // Used to delay draw updates
+
+var saveRow;
+var saveCol;
+var dataBoxHeight;
+var dataBoxWidth;
+
+
+var detailDendroHeight = 105;
+var detailDendroWidth = 105;
+var normDetailDendroMatrixHeight = 200;
+var rowDetailDendroMatrix,colDetailDendroMatrix;
+var DETAIL_SIZE_NORMAL_MODE = 502;
+var detailDataViewHeight = 502;
+var detailDataViewWidth = 502;
+var detailDataViewBoarder = 2;
+var zoomBoxSizes = [1,2,4,5,10,20,25,50];
+var labelSizeLimit = 8;
+var currentSearchItem;
+var labelLastClicked;
+
+var mouseDown = false;
+var dragOffsetX;
+var dragOffsetY;
+var detailPoint;
+var detailGrid = true;
+
+var mode = 'NORMAL';
+var isDrawn = false;
+
+//Call once to hook up detail drawing routines to a heat map and initialize the webGl 
+function initDetailDisplay() {
+	detCanvas = document.getElementById('detail_canvas');
+	labelElement = document.getElementById('labelDiv');
+
+	if (isSub) {
+ 		document.getElementById('summary_chm').style.display = 'none';
+ 		document.getElementById('divider').style.display = 'none';
+ 		document.getElementById('detail_chm').style.width = '100%';
+ 		document.getElementById('detail_buttons').style.display = '';
+ 		document.getElementById('split_btn').src= staticPath + "images/join.png";
+ 		document.getElementById('gear_btn').src= staticPath + "images/gearDis.png";
+ 		document.getElementById('pdf_btn').style.display = 'none';
+	}
+	
+	if (dataBoxWidth === undefined) {
+		setDetailDataSize(10);
+	}
+	if (heatMap.isInitialized() > 0) {
+		document.getElementById('detail_buttons').style.display = '';
+		detCanvas.width =  (detailDataViewWidth + calculateTotalClassBarHeight("row") + detailDendroWidth);
+		detCanvas.height = (detailDataViewHeight + calculateTotalClassBarHeight("column") + detailDendroHeight);
+		detSetupGl();
+		detInitGl();
+		createLabelMenus();
+		updateSelection();
+	}
+		
+	detCanvas.onmousedown = clickStart;
+	document.onmouseup = clickEnd;
+	detCanvas.onmousemove = handleMove;
+	detCanvas.onmouseleave = userHelpClose;
+	document.addEventListener("touchmove", function(e){
+		e.preventDefault();
+		if (e.touches){
+	    	if (e.touches.length > 1){
+	    		return false;
+	    	}
+	    }
+	})
+	detCanvas.addEventListener("touchstart", function(e){
+		userHelpClose();
+		clickStart(e);
+	}, false);
+	detCanvas.addEventListener("touchmove", function(e){
+		e.stopPropagation();
+		e.preventDefault();
+		handleMove(e);
+	}, false);
+	detCanvas.addEventListener("touchend", function(e){clickEnd(e)}, false);
+	
+	detCanvas.addEventListener("gestureend",function(e){
+		if (e.scale > 1){
+			detailDataZoomIn();
+		} else if (e.scale < 1){
+			detailDataZoomOut();
+		}
+	},false)
+	
+	
+	document.onkeydown = keyNavigate;
+}
+
+function clickStart(e){
+	userHelpClose();
+	dragOffsetX = e.touches ? e.touches[0].pageX : e.pageX;
+	dragOffsetY = e.touches ? e.touches[0].pageY : e.pageY;
+
+    mouseDown = true;
+}
+function clickEnd(e){
+	mouseDown = false;
+	var dragEndX = e.touches ? e.touches[0].pageX : e.pageX;
+	var dragEndY = e.touches ? e.touches[0].pageY : e.pageY;
+	var rowElementSize = dataBoxWidth * detCanvas.clientWidth/detCanvas.width;
+    var colElementSize = dataBoxHeight * detCanvas.clientHeight/detCanvas.height;
+	if (Math.abs(dragEndX - dragOffsetX) < colElementSize/10 && Math.abs(dragEndY - dragOffsetY) < rowElementSize/10){
+		userHelpOpen(e);
+	}
+}
+
+function handleDrag(e) {
+    if(!mouseDown) return;
+    var rowElementSize = dataBoxWidth * detCanvas.clientWidth/detCanvas.width;
+    var colElementSize = dataBoxHeight * detCanvas.clientHeight/detCanvas.height;
+    if (e.touches){
+    	if (e.touches.length > 1){
+    		return false;
+    	}
+    }
+    var xDrag = e.touches ? e.touches[0].pageX - dragOffsetX : e.pageX - dragOffsetX;
+    var yDrag = e.touches ? e.touches[0].pageY - dragOffsetY : e.pageY - dragOffsetY;
+    
+    if ((Math.abs(xDrag/rowElementSize) > 1) || 
+    	(Math.abs(yDrag/colElementSize) > 1)    ) {
+    	currentRow = Math.floor(currentRow - (yDrag/colElementSize));
+    	currentCol = Math.floor(currentCol - (xDrag/rowElementSize));
+    	
+	    dragOffsetX = e.touches ? e.touches[0].pageX : e.pageX;
+	    dragOffsetY = e.touches ? e.touches[0].pageY : e.pageY;
+	    var numRows = heatMap.getNumRows(MatrixManager.DETAIL_LEVEL);
+	    var numCols = heatMap.getNumColumns(MatrixManager.DETAIL_LEVEL);
+	    checkRow();
+	    checkColumn();
+	 
+	    updateSelection();
+   }
+    return false;
+}	
+
+function handleMove(e) {
+    // Do not clear help if the mouse position did not change. Repeated firing of the mousemove event can happen on random 
+    // machines in all browsers but FireFox. There are varying reasons for this so we check and exit if need be.
+	if(old_mouse_pos[0] != e.clientX || old_mouse_pos[1] != e.clientY) {
+		userHelpClose();
+		old_mouse_pos = [e.clientX, e.clientY];
+	} 
+	if (mouseDown){
+		handleDrag(e);
+	} 
+}
+ 
+function getColClassPixelHeight() {
+	var classbarHeight = calculateTotalClassBarHeight("column");
+	return detCanvas.clientHeight*(classbarHeight/detCanvas.height);
+}
+
+function getRowClassPixelWidth() {
+	var classbarWidth = calculateTotalClassBarHeight("row");
+	return detCanvas.clientWidth*(classbarWidth/detCanvas.width);
+}
+
+function getColDendroPixelHeight() {
+	return detCanvas.clientHeight*(detailDendroHeight/detCanvas.height);
+}
+
+function getRowDendroPixelWidth() {
+	return detCanvas.clientWidth*(detailDendroWidth/detCanvas.width);
+}
+
+function isOnObject(e,type) {
+    var rowClassWidthPx =  getRowClassPixelWidth();
+    var colClassHeightPx = getColClassPixelHeight();
+    var rowDendroWidthPx =  getRowDendroPixelWidth();
+    var colDendroHeightPx = getColDendroPixelHeight();
+    if (e.layerY > colClassHeightPx + colDendroHeightPx) { 
+    	if  ((type == "map") && e.layerX > rowClassWidthPx + rowDendroWidthPx) {
+    		return true;
+    	}
+    	if  ((type == "rowClass") && e.layerX < rowClassWidthPx + rowDendroWidthPx && e.layerX > rowDendroWidthPx) {
+    		return true;
+    	}
+    } else if (e.layerY > colDendroHeightPx) {
+    	if  ((type == "colClass") && e.layerX > rowClassWidthPx + rowDendroWidthPx) {
+    		return true;
+    	}
+    }
+    return false;
+}	
+
+function detailDataZoomIn() {
+	userHelpClose();	
+	if (mode == 'NORMAL') {
+		var current = zoomBoxSizes.indexOf(dataBoxWidth);
+		if (current < zoomBoxSizes.length - 1) {
+			setDetailDataSize (zoomBoxSizes[current+1]);
+			updateSelection();
+		}
+	} else if ((mode == 'RIBBONH') || (mode == 'RIBBONH_DETAIL')) {
+		var current = zoomBoxSizes.indexOf(dataBoxHeight);
+		if (current < zoomBoxSizes.length - 1) {
+			setDetailDataHeight (zoomBoxSizes[current+1]);
+			updateSelection();
+		}
+	} else if ((mode == 'RIBBONV') || (mode == 'RIBBONV_DETAIL')) {
+		var current = zoomBoxSizes.indexOf(dataBoxWidth);
+		if (current < zoomBoxSizes.length - 1) {
+			setDetailDataWidth(zoomBoxSizes[current+1]);
+			updateSelection();
+		}
+	}
+}	
+
+function detailDataZoomOut() {
+	userHelpClose();	
+	if (mode == 'NORMAL') {
+		var current = zoomBoxSizes.indexOf(dataBoxWidth);
+		if ((current > 0) &&
+		    (Math.floor((detailDataViewHeight-detailDataViewBoarder)/zoomBoxSizes[current-1]) <= heatMap.getNumRows(MatrixManager.DETAIL_LEVEL)) &&
+		    (Math.floor((detailDataViewWidth-detailDataViewBoarder)/zoomBoxSizes[current-1]) <= heatMap.getNumColumns(MatrixManager.DETAIL_LEVEL))){
+			setDetailDataSize (zoomBoxSizes[current-1]);
+			updateSelection();
+		}	
+	} else if ((mode == 'RIBBONH') || (mode == 'RIBBONH_DETAIL')) {
+		var current = zoomBoxSizes.indexOf(dataBoxHeight);
+		if ((current > 0) &&
+		    (Math.floor((detailDataViewHeight-detailDataViewBoarder)/zoomBoxSizes[current-1]) <= heatMap.getNumRows(MatrixManager.DETAIL_LEVEL))) {
+			setDetailDataHeight (zoomBoxSizes[current-1]);
+			updateSelection();
+		}	
+	} else if ((mode == 'RIBBONV') || (mode == 'RIBBONV_DETAIL')){
+		var current = zoomBoxSizes.indexOf(dataBoxWidth);
+		if ((current > 0) &&
+		    (Math.floor((detailDataViewWidth-detailDataViewBoarder)/zoomBoxSizes[current-1]) <= heatMap.getNumColumns(MatrixManager.DETAIL_LEVEL))){
+			setDetailDataWidth (zoomBoxSizes[current-1]);
+			updateSelection();
+		}	
+	}
+}
+
+//How big each data point should be in the detail pane.  
+function setDetailDataSize(size) {
+	setDetailDataWidth (size);
+	setDetailDataHeight(size);
+}
+
+//How big each data point should be in the detail pane.  
+function setDetailDataWidth(size) {
+	var prevDataPerRow = dataPerRow;
+	dataBoxWidth = size;
+	setDataPerRowFromDet(Math.floor((detailDataViewWidth-detailDataViewBoarder)/dataBoxWidth));
+
+	//Adjust the current column based on zoom but don't go outside or the heat map matrix dimensions.
+	if (prevDataPerRow != null) {
+		if (prevDataPerRow > dataPerRow)
+			currentCol += Math.floor((prevDataPerRow - dataPerRow) / 2);
+		else
+			currentCol -= Math.floor((dataPerRow - prevDataPerRow) / 2);
+		checkColumn();
+	}
+}
+
+//How big each data point should be in the detail pane.  
+function setDetailDataHeight(size) {
+	var prevDataPerCol = dataPerCol;
+	dataBoxHeight = size;
+	setDataPerColFromDet(Math.floor((detailDataViewHeight-detailDataViewBoarder)/dataBoxHeight));
+	
+	//Adjust the current row but don't go outside of the current heat map dimensions
+	if (prevDataPerCol != null) {
+		if (prevDataPerCol > dataPerCol)
+			currentRow += Math.floor((prevDataPerCol - dataPerCol) / 2);
+		else
+			currentRow -= Math.floor((dataPerCol - prevDataPerCol) / 2);
+		checkRow();
+	}
+}
+
+//How much data are we showing per row - determined by dataBoxWidth and detailDataViewWidth
+function getDetailDataPerRow() {
+	return dataPerRow;
+}
+
+//How much data are we showing per row - determined by dataBoxWidth and detailDataViewWidth
+function getDetailDataPerCol () {
+	return dataPerCol;
+}
+
+function detailHRibbonButton () {
+	clearDendroSelection();
+	detailHRibbon();
+}
+
+function detailVRibbonButton () {
+	clearDendroSelection();
+	detailVRibbon();
+}
+
+//Change to horizontal ribbon view.  Note there is a standard full ribbon view and also a sub-selection
+//ribbon view if the user clicks on the dendrogram.  If a dendrogram selection is in effect, then
+//selectedStart and selectedStop will be set.
+function detailHRibbon () {
+	userHelpClose();	
+	var previousMode = mode;
+	var prevWidth = dataBoxWidth;
+	saveCol = currentCol;
+	
+		
+	mode='RIBBONH';
+	setButtons();
+	
+	// If normal (full) ribbon, set the width of the detail display to the size of the horizontal ribbon view
+	// and data size to 1.
+	if (selectedStart == null || selectedStart == 0) {
+		detailDataViewWidth = heatMap.getNumColumns(MatrixManager.RIBBON_HOR_LEVEL) + detailDataViewBoarder;
+		setDetailDataWidth(1);
+		currentCol = 1;
+	} else {
+		var selectionSize = selectedStop - selectedStart + 1;
+		if (selectionSize < 500) {
+			mode='RIBBONH_DETAIL'
+		} else {
+			var rvRate = heatMap.getColSummaryRatio(MatrixManager.RIBBON_HOR_LEVEL);
+			selectionSize = Math.floor(selectionSize/rvRate);
+		}
+		var width = Math.max(1, Math.floor(500/selectionSize));
+		detailDataViewWidth = (selectionSize * width) + detailDataViewBoarder;
+		setDetailDataWidth(width);	
+		currentCol = selectedStart;
+	}
+	
+	detailDataViewHeight = DETAIL_SIZE_NORMAL_MODE;
+	if ((previousMode=='RIBBONV') || (previousMode == 'RIBBONV_DETAIL')) {
+		setDetailDataHeight(prevWidth);
+		currentRow=saveRow;
+	}	
+	
+	detCanvas.width =  (detailDataViewWidth + calculateTotalClassBarHeight("row") + detailDendroWidth);
+	detCanvas.height = (detailDataViewHeight + calculateTotalClassBarHeight("column") + detailDendroHeight);
+	detSetupGl();
+	detInitGl();
+	drawDetailHeatMap();
+	updateSelection();
+	highlightAllColLabels();
+	document.getElementById("viewport").setAttribute("content", "height=device-height");
+    document.getElementById("viewport").setAttribute("content", "");
+}
+
+function detailVRibbon () {
+	userHelpClose();	
+	var previousMode = mode;
+	var prevHeight = dataBoxHeight;
+	saveRow = currentRow;
+	
+	mode='RIBBONV';
+	setButtons();
+
+	// If normal (full) ribbon, set the width of the detail display to the size of the horizontal ribbon view
+	// and data size to 1.
+	if (selectedStart == null || selectedStart == 0) {
+		detailDataViewHeight = heatMap.getNumRows(MatrixManager.RIBBON_VERT_LEVEL) + detailDataViewBoarder;
+		setDetailDataHeight(1);
+		currentRow = 1;
+	} else {
+		var selectionSize = selectedStop - selectedStart + 1;
+		if (selectionSize < 500) {
+			mode = 'RIBBONV_DETAIL';
+		} else {
+			var rvRate = heatMap.getRowSummaryRatio(MatrixManager.RIBBON_VERT_LEVEL);
+			selectionSize = Math.floor(selectionSize / rvRate);			
+		}
+		var height = Math.max(1, Math.floor(500/selectionSize));
+    	detailDataViewHeight = (selectionSize * height) + detailDataViewBoarder;
+		setDetailDataHeight(height);
+		currentRow = selectedStart;
+	}
+	
+	detailDataViewWidth = DETAIL_SIZE_NORMAL_MODE;
+	if ((previousMode=='RIBBONH') || (previousMode=='RIBBONH_DETAIL')) {
+		setDetailDataWidth(prevHeight);
+		currentCol = saveCol;
+	}
+	
+	detCanvas.width =  (detailDataViewWidth + calculateTotalClassBarHeight("row") + detailDendroWidth);
+	detCanvas.height = (detailDataViewHeight + calculateTotalClassBarHeight("column") + detailDendroHeight);
+	detSetupGl();
+	detInitGl();
+	drawDetailHeatMap();
+	updateSelection();
+	highlightAllRowLabels();
+	document.getElementById("viewport").setAttribute("content", "height=device-height");
+    document.getElementById("viewport").setAttribute("content", "");
+}
+
+function detailNormal () {
+	userHelpClose();	
+	var previousMode = mode;
+	mode = 'NORMAL';
+	setButtons();
+	detailDataViewHeight = DETAIL_SIZE_NORMAL_MODE;
+	detailDataViewWidth = DETAIL_SIZE_NORMAL_MODE;
+	if ((previousMode=='RIBBONV') || (previousMode=='RIBBONV_DETAIL')) {
+		setDetailDataSize(dataBoxWidth);
+		currentRow = saveRow;
+	} else if ((previousMode=='RIBBONH') || (previousMode=='RIBBONH_DETAIL')) {
+		setDetailDataSize(dataBoxHeight);
+		currentCol = saveCol;
+	} else {
+		
+	}	
+	detCanvas.width =  (detailDataViewWidth + calculateTotalClassBarHeight("row") + detailDendroWidth);
+	detCanvas.height = (detailDataViewHeight + calculateTotalClassBarHeight("column") + detailDendroHeight);
+	detSetupGl();
+	detInitGl();
+	clearDendroSelection();
+	drawDetailHeatMap();
+	updateSelection();
+	document.getElementById("viewport").setAttribute("content", "height=device-height");
+    document.getElementById("viewport").setAttribute("content", "");
+}
+
+function setButtons() {
+	var full = document.getElementById('full_btn');
+	var ribbonH = document.getElementById('ribbonH_btn');
+	var ribbonV = document.getElementById('ribbonV_btn');
+	full.src= staticPath+ "images/full.png";
+	ribbonH.src= staticPath + "images/ribbonH.png";
+	ribbonV.src= staticPath + "images/ribbonV.png";
+	if (mode=='RIBBONV')
+		ribbonV.src= staticPath + "images/ribbonV_selected.png";
+	else if (mode == "RIBBONH")
+		ribbonH.src= staticPath + "images/ribbonH_selected.png";
+	else
+		full.src= staticPath + "images/full_selected.png";	
+}
+
+
+//Called when split/join button is pressed
+function detailSplit(){
+	userHelpClose();	
+	// If the summary and detail are in a single browser window, this is a split action.  
+	if (!isSub) {
+		//Write current selection settings to the local storage
+		hasSub=true;
+		updateSelection();
+		
+		//Create a new detail browser window
+		detWindow = window.open(window.location.href + '&sub=true', '_blank', 'modal=yes, width=' + (window.screen.availWidth / 2) + ', height='+ window.screen.availHeight + ',top=0, left=' + (window.screen.availWidth / 2));
+		detWindow.moveTo(window.screen.availWidth / 2, 0);
+		detWindow.onbeforeunload = function(){rejoinNotice(),detailJoin(),hasSub=false;} // when you close the subwindow, it will return to the original window
+		var detailDiv = document.getElementById('detail_chm');
+		detailDiv.style.display = 'none';
+		var dividerDiv = document.getElementById('divider');
+		dividerDiv.style.display = 'none';
+		//In summary window, hide the action buttons and expand the summary to 100% of the window.
+		var detailButtonDiv = document.getElementById('detail_buttons');
+		detailButtonDiv.style.display = 'none';
+		var summaryDiv = document.getElementById('summary_chm');
+		summaryDiv.style.width = '100%';
+	} else {
+		updateSelection();
+		rejoinNotice();
+		window.close();
+	}
+}
+
+//Called when a separate detail window is joined back into the main window.
+function detailJoin() {
+	var detailDiv = document.getElementById('detail_chm');
+	detailDiv.style.display = '';
+	detailDiv.style.width = '48%';
+	var detailButtonDiv = document.getElementById('detail_buttons');
+	detailButtonDiv.style.display = '';
+	var dividerDiv = document.getElementById('divider');
+	dividerDiv.style.display = '';
+	var summaryDiv = document.getElementById('summary_chm');
+	summaryDiv.style.width = '48%';
+	initFromLocalStorage();
+}
+
+
+// Callback that is notified every time there is an update to the heat map 
+// initialize, new data, etc.  This callback draws the summary heat map.
+function processDetailMapUpdate (event, level) {
+
+	if (event == MatrixManager.Event_INITIALIZED) {
+		detailInit();
+	} else {
+		//Data tile update - wait a bit to see if we get another new tile quickly, then draw
+		if (detEventTimer != 0) {
+			//New tile arrived - reset timer
+			clearTimeout(detEventTimer);
+		}
+		detEventTimer = setTimeout(drawDetailHeatMap, 200);
+	} 
+}
+ 
+//Perform all initialization functions for Detail heat map
+function detailInit() {
+	var dendroGram = heatMap.getDendrogram();
+	if (!heatMap.showRowDendrogram("DETAIL")) {
+		detailDendroWidth = 15;
+	} else {
+		detailDendroWidth = parseInt(dendroGram['row_dendro_height'])+5;
+	}
+	if (!heatMap.showColDendrogram("DETAIL")) {
+		detailDendroHeight = 15;
+	} else {
+		detailDendroHeight = parseInt(dendroGram['col_dendro_height'])+5;
+	}
+	document.getElementById('detail_buttons').style.display = '';
+	detCanvas.width =  (detailDataViewWidth + calculateTotalClassBarHeight("row") + detailDendroWidth);
+	detCanvas.height = (detailDataViewHeight + calculateTotalClassBarHeight("column") + detailDendroHeight);
+	createLabelMenus();
+	detSetupGl();
+	detInitGl();
+	if (isSub)
+		initFromLocalStorage();
+	else
+		updateSelection();
+}
+
+function drawDetailHeatMap() {
+ 	
+	if ((currentRow == null) || (currentRow == 0)) {
+		return;
+	}
+	var colorMap = heatMap.getColorMapManager().getColorMap("dl1");
+	var rowClassBarWidth = calculateTotalClassBarHeight("row");
+	var searchRows = getSearchRows();
+	var searchCols = getSearchCols();
+	var searchGridColor = [0,0,0];
+	var regularGridColor = [255,255,255];
+	var detDataPerRow = getCurrentDetDataPerRow();
+	var detDataPerCol = getCurrentDetDataPerCol();
+ 
+	//Build a horizontal grid line for use between data lines. Tricky because some dots will be selected color if a column is in search results.
+	var gridLine = new Uint8Array(new ArrayBuffer((detailDendroWidth + rowClassBarWidth + detailDataViewWidth) * BYTE_PER_RGBA));
+	if (detailGrid == true) {
+		var linePos = (detailDendroWidth+rowClassBarWidth)*BYTE_PER_RGBA;
+		gridLine[linePos]=0; gridLine[linePos+1]=0;gridLine[linePos+2]=0;gridLine[linePos+3]=255;linePos+=BYTE_PER_RGBA;
+		for (var j = 0; j < detDataPerRow; j++) {
+			var gridColor = ((searchCols.indexOf(currentCol+j) > -1) || (searchCols.indexOf(currentCol+j+1) > -1)) ? searchGridColor : regularGridColor;
+			for (var k = 0; k < dataBoxWidth; k++) {
+				if (k==dataBoxWidth-1 && detailGrid == true && dataBoxWidth > labelSizeLimit ){ // should the grid line be drawn?
+					gridLine[linePos] = gridColor[0]; gridLine[linePos+1] = gridColor[1]; gridLine[linePos+2] = gridColor[2];	gridLine[linePos+3] = 255;
+				} else {
+					gridLine[linePos]=regularGridColor[0]; gridLine[linePos + 1]=regularGridColor[1]; gridLine[linePos + 2]=regularGridColor[2]; gridLine[linePos + 3]=255;
+				}
+				linePos += BYTE_PER_RGBA;
+			}
+		}
+		gridLine[linePos]=0; gridLine[linePos+1]=0;gridLine[linePos+2]=0;gridLine[linePos+3]=255;linePos+=BYTE_PER_RGBA;
+	}
+	
+	//Setup texture to draw on canvas.
+	
+	//Draw black boarder line
+	var pos = (rowClassBarWidth+detailDendroWidth)*BYTE_PER_RGBA;
+	for (var i = 0; i < detailDataViewWidth; i++) {
+		detTexPixels[pos]=0;detTexPixels[pos+1]=0;detTexPixels[pos+2]=0;detTexPixels[pos+3]=255;pos+=BYTE_PER_RGBA;
+	}
+		
+	//Needs to go backward because WebGL draws bottom up.
+	var line = new Uint8Array(new ArrayBuffer((rowClassBarWidth + detailDendroWidth + detailDataViewWidth) * BYTE_PER_RGBA));
+	for (var i = detDataPerCol-1; i >= 0; i--) {
+		var linePos = (rowClassBarWidth + detailDendroWidth)*BYTE_PER_RGBA;
+		//Add black boarder
+		line[linePos]=0; line[linePos+1]=0;line[linePos+2]=0;line[linePos+3]=255;linePos+=BYTE_PER_RGBA;
+		for (var j = 0; j < detDataPerRow; j++) { // for every data point...
+			var val = heatMap.getValue(getLevelFromMode(MatrixManager.DETAIL_LEVEL), getCurrentDetRow()+i, getCurrentDetCol()+j);
+			var color = colorMap.getColor(val);
+			var gridColor = ((searchCols.indexOf(currentCol+j) > -1) || (searchCols.indexOf(currentCol+j+1) > -1)) ? searchGridColor : regularGridColor;
+
+			//For each data point, write it several times to get correct data point width.
+			for (var k = 0; k < dataBoxWidth; k++) {
+				if (k==dataBoxWidth-1 && detailGrid == true && dataBoxWidth > labelSizeLimit ){ // should the grid line be drawn?
+					line[linePos] = gridColor[0]; line[linePos+1] = gridColor[1]; line[linePos+2] = gridColor[2];	line[linePos+3] = 255;
+				} else {
+					line[linePos] = color['r'];	line[linePos + 1] = color['g'];	line[linePos + 2] = color['b'];	line[linePos + 3] = color['a'];
+				}
+				linePos += BYTE_PER_RGBA;
+			}
+		}
+		line[linePos]=0; line[linePos+1]=0;line[linePos+2]=0;line[linePos+3]=255;linePos+=BYTE_PER_RGBA;
+
+
+		//Write each line several times to get correct data point height.
+		for (dup = 0; dup < dataBoxHeight; dup++) {
+			if (dup == dataBoxHeight-1 && detailGrid == true && dataBoxHeight > labelSizeLimit){ // do we draw gridlines?
+				if ((searchRows.indexOf(currentRow+i) > -1) || (searchRows.indexOf(currentRow+i-1) > -1)) {
+					pos += (rowClassBarWidth + detailDendroWidth)*BYTE_PER_RGBA;
+					for (var k = 0; k < detailDataViewWidth; k++) {
+						detTexPixels[pos]=searchGridColor[0];detTexPixels[pos+1]=searchGridColor[1];detTexPixels[pos+2]=searchGridColor[2];detTexPixels[pos+3]=255;pos+=BYTE_PER_RGBA;
+					}					
+				} else {
+					for (k = 0; k < line.length; k++) {
+						detTexPixels[pos]=gridLine[k];
+						pos++;
+					}
+				}	
+			} else {
+				for (k = 0; k < line.length; k++) {
+					detTexPixels[pos]=line[k];
+					pos++;
+				}
+			}
+		}
+	}
+
+	//Draw black boarder line
+	pos += (rowClassBarWidth + detailDendroWidth)*BYTE_PER_RGBA;
+	for (var i = 0; i < detailDataViewWidth; i++) {
+		detTexPixels[pos]=0;detTexPixels[pos+1]=0;detTexPixels[pos+2]=0;detTexPixels[pos+3]=255;pos+=BYTE_PER_RGBA;
+	}
+	clearDetailDendrograms();
+	if (heatMap.showRowDendrogram("DETAIL")) {
+		rowDetailDendroMatrix = buildDetailDendroMatrix('Row', currentRow, currentRow+dataPerCol, heatMap.getNumRows(MatrixManager.DETAIL_LEVEL)/dataPerCol);
+		detailDrawRowDendrogram(detTexPixels);
+	}
+	if (heatMap.showColDendrogram("DETAIL")) {
+		colDetailDendroMatrix = buildDetailDendroMatrix('Column', currentCol, currentCol+dataPerRow, heatMap.getNumColumns(MatrixManager.DETAIL_LEVEL)/dataPerRow);
+		detailDrawColDendrogram(detTexPixels);
+	}
+	//Draw column classification bars.
+	detailDrawColClassBars();
+	detailDrawRowClassBars();
+
+	
+	//WebGL code to draw the summary heat map.
+	det_gl.activeTexture(det_gl.TEXTURE0);
+	det_gl.texImage2D(
+			det_gl.TEXTURE_2D, 
+			0, 
+			det_gl.RGBA, 
+			detTextureParams['width'], 
+			detTextureParams['height'], 
+			0, 
+			det_gl.RGBA,
+			det_gl.UNSIGNED_BYTE, 
+			detTexPixels);
+	det_gl.uniform2fv(detUScale, detCanvasScaleArray);
+	det_gl.uniform2fv(detUTranslate, detCanvasTranslateArray);
+	det_gl.uniform2fv(detUBoxLeftTop, detCanvasBoxLeftTopArray);
+	det_gl.uniform2fv(detUBoxRightBottom, detCanvasBoxRightBottomArray);
+	det_gl.uniform1f(detUBoxThickness, 0.002);
+	det_gl.uniform4fv(detUBoxColor, [1.0, 1.0, 0.0, 1.0]);
+	det_gl.drawArrays(det_gl.TRIANGLE_STRIP, 0, det_gl.buffer.numItems);
+
+	clearLabels();
+	drawRowLabels();
+	drawColLabels();
+	detailDrawColClassBarLabels();
+	detailDrawRowClassBarLabels();
+}
+
+function detailResize() {
+	 clearLabels();
+	 drawRowLabels();
+	 drawColLabels();
+	 detailDrawColClassBarLabels();
+	 detailDrawRowClassBarLabels();
+}
+
+/***********************************************************
+ * Search Functions Section
+ ***********************************************************/
+
+//Called when search string is entered.
+function detailSearch() {
+	var searchElement = document.getElementById('search_text');
+	var searchString = searchElement.value;
+	searchItems = [];
+	var tmpSearchItems = searchString.split(/[;, ]+/);
+	itemsFound = [];
+	
+	//Put labels into the global search item list if they match a user search string.
+	//Regular expression is built for partial matches if the search string contains '*'.
+	//toUpperCase is used to make the search case insensitive.
+	var labels = heatMap.getRowLabels()["labels"];
+	for (var j = 0; j < tmpSearchItems.length; j++) {
+		var reg = null;
+		if (tmpSearchItems[j].indexOf("*") > -1) {
+			reg = new RegExp("^" + tmpSearchItems[j].toUpperCase().replace(/\*/g, ".*") + "$");
+		}
+		for (var i = 0; i < labels.length; i++) {
+			if ((labels[i].toUpperCase() == tmpSearchItems[j].toUpperCase()) ||
+				((reg != null) && reg.test(labels[i].toUpperCase()))){
+				searchItems.push({'axis' : 'Row', 'label': labels[i]});
+				if (itemsFound.indexOf(tmpSearchItems[j]) == -1)
+					itemsFound.push(tmpSearchItems[j]);
+			}
+		}	
+	}
+
+	labels = heatMap.getColLabels()["labels"];
+	for (var j = 0; j < tmpSearchItems.length; j++) {
+		var reg = null;
+		if (tmpSearchItems[j].indexOf("*") > -1) {
+			reg = new RegExp("^" + tmpSearchItems[j].toUpperCase().replace(/\*/g, ".*") + "$");
+		}
+		for (var i = 0; i < labels.length; i++) {
+			if ((labels[i].toUpperCase() == tmpSearchItems[j].toUpperCase()) ||
+				((reg != null) && reg.test(labels[i].toUpperCase()))){
+				searchItems.push({'axis' : 'Column', 'label': labels[i]});
+				if (itemsFound.indexOf(tmpSearchItems[j]) == -1)
+					itemsFound.push(tmpSearchItems[j]);
+			}
+		}	
+	}
+
+	//Jump to the first match
+	var srchText = document.getElementById('search_text');
+	if (searchItems.length > 0) {
+		currentSearchItem = searchItems[0];
+		goToCurrentSearchItem();
+		if (itemsFound.length != tmpSearchItems.length) {
+			srchText.style.backgroundColor = "rgba(255,255,0,0.3)";
+		}
+	} else {
+		if (searchString != null && searchString.length> 0) {
+			srchText.style.backgroundColor = "rgba(255,0,0,0.3)";
+		}	
+		//Clear previous matches when search is empty.
+		updateSelection();
+	}
+}
+
+function goToCurrentSearchItem() {
+	var row = findRowLabel(currentSearchItem.label);
+	if (row > -1) {
+		currentRow = row;
+		if ((mode == 'RIBBONV') && selectedStart!= 0 && (currentRow < selectedStart-1 || selectedStop-1 < currentRow)){
+			showSearchError(1);
+		} else if (mode == 'RIBBONV' && selectedStart == 0){
+			showSearchError(2);
+		} 
+		checkRow();
+	} else {
+		currentCol = findColLabel(currentSearchItem.label);
+		if ((mode == 'RIBBONH') && selectedStart!= 0 && (currentCol < selectedStart-1 || selectedStop-1 < currentCol )){
+			showSearchError(1)
+		} else if (mode == 'RIBBONH' && selectedStart == 0){
+			showSearchError(2);
+		} 
+		checkColumn();
+	}
+	document.getElementById('prev_btn').style.display='';
+	document.getElementById('next_btn').style.display='';
+	document.getElementById('cancel_btn').style.display='';
+	updateSelection();
+}
+
+//Search the row and column labels - return position if found or -1 if not found.
+function findRowLabel(name){
+	var labels = heatMap.getRowLabels()["labels"];
+	for (var i = 0; i < labels.length; i++) {
+		if (labels[i].toUpperCase() == name.toUpperCase())
+			return i;
+	}
+	return -1;
+}	
+	
+function findColLabel(name) {	
+	var labels = heatMap.getColLabels()["labels"];
+	for (var i = 0; i < labels.length; i++) {
+		if (labels[i].toUpperCase() == name.toUpperCase())
+			return i;
+	}
+	return -1;
+}
+
+//Go to next search item
+function searchNext() {
+	var pos = findCurrentSelection();
+	if (pos == searchItems.length-1)
+		pos = 0;
+	else 
+		pos++;
+	currentSearchItem = searchItems[pos];
+	goToCurrentSearchItem();	
+}
+
+//Go back to previous search item.
+function searchPrev() {
+	var pos = findCurrentSelection();
+	if (pos == 0)
+		pos = searchItems.length-1;
+	else 
+		pos--;
+	currentSearchItem = searchItems[pos];
+	goToCurrentSearchItem();	
+}
+
+//Called when red 'X' is clicked.
+function clearSearch(){
+	var searchElement = document.getElementById('search_text');
+	searchElement.value = "";
+	clearSrchBtns();
+	detailSearch();
+}
+
+function clearSrchBtns() {
+	if ((event != null) && (event.keyCode == 13))
+		return;
+	
+	document.getElementById('prev_btn').style.display='none';
+	document.getElementById('next_btn').style.display='none';	
+	document.getElementById('cancel_btn').style.display='none';	
+	var srchText = document.getElementById('search_text');
+	srchText.style.backgroundColor = "white";
+}
+
+function findCurrentSelection() {
+	if (currentSearchItem === undefined){
+		return 0;
+	}
+	for (var i = 0; i < searchItems.length; i++) {
+		if (currentSearchItem.label == searchItems[i].label && currentSearchItem.axis == searchItems[i].axis)
+			return i;
+	}
+	return 0;
+}
+
+//Return the column number of any columns meeting the current user search.
+function getSearchCols() {
+	var selected = [];
+	for (var i = 0; i < searchItems.length; i++) {
+		var col = findColLabel(searchItems[i].label);
+		if (col > -1)
+			selected.push(col+1);
+	}
+	return selected;	
+}
+
+//Return row numbers of any rows meeting current user search.
+function getSearchRows() {
+	var selected = [];
+	for (var i = 0; i < searchItems.length; i++) {
+		var row = findRowLabel(searchItems[i].label);
+		if (row > -1)
+			selected.push(row+1);
+	}
+	return selected;	
+}
+
+/***********************************************************
+ * End - Search Functions
+ ***********************************************************/
+
+function clearLabels() {
+	var oldLabels = document.getElementsByClassName("DynamicLabel");
+	while (oldLabels.length > 0) {
+		labelElement.removeChild(oldLabels[0]);
+	}
+
+}
+
+function drawRowLabels() {
+	var headerSize = 0;
+	var colHeight = calculateTotalClassBarHeight("column") + detailDendroHeight;
+	if (colHeight > 0) {
+		headerSize = detCanvas.clientHeight * (colHeight / (detailDataViewHeight + colHeight));
+	}
+	var skip = (detCanvas.clientHeight - headerSize) / dataPerCol;
+	var fontSize = Math.min(skip - 2, 11);
+	var start = Math.max((skip - fontSize)/2, 0) + headerSize;
+	var labels = heatMap.getRowLabels()["labels"];
+	
+	
+	if (skip > labelSizeLimit) {
+		for (var i = currentRow; i < currentRow + dataPerCol; i++) {
+			var xPos = detCanvas.clientWidth + 3;
+			var yPos = start + ((i-currentRow) * skip);
+			addLabelDiv(labelElement, 'detail_row' + i, 'DynamicLabel', labels[i-1], xPos, yPos, fontSize, 'F');
+		}
+	}
+}
+
+
+function drawColLabels() {
+	var headerSize = 0;
+	var rowHeight = calculateTotalClassBarHeight("row") + detailDendroWidth;
+	if (rowHeight > 0) {
+		headerSize = detCanvas.clientWidth * (rowHeight / (detailDataViewWidth + rowHeight));
+	}
+	var skip = (detCanvas.clientWidth - headerSize) / dataPerRow;
+	var fontSize = Math.min(skip - 2, 11);
+	var start = headerSize + fontSize + Math.max((skip - fontSize)/2, 0) + 3;
+	var labels = heatMap.getColLabels()["labels"];
+	var labelLen = getMaxLength(labels);
+		
+	if (skip > labelSizeLimit) {
+		var yPos = detCanvas.clientHeight + 4;
+		for (var i = currentCol; i < currentCol + dataPerRow; i++) {
+			var xPos = start + ((i-currentCol) * skip);
+			addLabelDiv(labelElement, 'detail_col' + i, 'DynamicLabel', labels[i-1], xPos, yPos, fontSize, 'T');
+		}
+	}
+}
+
+function addLabelDiv(parent, id, className, text, left, top, fontSize, rotate) {
+	var div = document.createElement('div');
+	div.id = id;
+	div.className = className;
+	div.innerHTML = text;
+	if (div.classList.contains('ClassBar')){
+		div.setAttribute('axis','ColumnClass');
+	} else {
+		div.setAttribute('axis', 'Row');
+	}
+	if (labelIndexInSearch(text,"Row") > -1 || labelIndexInSearch(text, "Column") > -1 || labelIndexInSearch(text, "ColumnClass") > -1 || labelIndexInSearch(text, "RowClass") > -1) 
+		div.classList.add('searchItem');
+	if (text == "<") {
+		div.style.backgroundColor = "rgba(255,255,0,0.2)";
+	}	
+	if (rotate == 'T') {
+		div.style.transformOrigin = 'left top';
+		div.style.transform = 'rotate(90deg)';
+		div.style.webkitTransformOrigin = "left top";
+		div.style.webkitTransform = "rotate(90deg)";
+		if (div.classList.contains('ClassBar')){
+			div.setAttribute('axis','RowClass');
+		} else {
+			div.setAttribute('axis','Column');
+		}
+	}
+	div.style.position = "absolute";
+	div.style.left = left;
+	div.style.top = top;
+	div.style.fontSize = fontSize.toString() +'pt';
+	div.style.fontFamily = 'times new roman';
+	div.style.fontWeight = 'bold';
+	div.addEventListener('click',labelClick,false);
+	div.addEventListener('contextmenu',labelRightClick,false);
+	
+	parent.appendChild(div);
+}
+
+
+// Get max label length
+function getMaxLength(list) {
+	var len = 0;
+	for (var i = 0; i < list.length; i++){
+		if (list[i].length > len)
+			len = list[i].length;
+	}
+	return len;
+}
+
+function labelClick(e){
+	if (e.shiftKey){ // shift + click
+		var selection = window.getSelection();
+		var focusNode = selection.focusNode.parentElement;
+		var focusIndex = Number(selection.focusNode.parentElement.id.substring(10)); // id = detail_rowX
+		if (labelLastClicked != undefined && labelLastClicked.axis == focusNode.getAttribute('axis')){ // if label in the same axis was clicked last, highlight all
+			var anchorIndex = labelLastClicked.index;
+			var anchorNode = document.getElementById('detail_' + labelLastClicked.axis.toLowerCase().substring(0,3) + labelLastClicked.index);
+			var currentNode = (anchorIndex < focusIndex) ? anchorNode: focusNode; 
+			var range = Math.abs(focusIndex-anchorIndex);
+			for (var i =0; i < range+1; i++){
+				if (labelIndexInSearch(currentNode.innerHTML, currentNode.getAttribute('axis')) == -1){
+					searchItems.push({'axis':currentNode.getAttribute('axis'), 'label':currentNode.innerHTML});
+				}
+				currentNode = currentNode.nextSibling;
+			}
+		} else { // otherwise, treat as normal click
+			clearSearchItems(this.getAttribute('axis'));
+			var labelIndex = labelIndexInSearch(this.innerHTML, this.getAttribute('axis'));
+			if (labelIndex > -1){
+				searchItems.splice(labelIndex, 1);
+			} else {
+				searchItems.push({'axis':this.getAttribute('axis'), 'label':this.innerHTML});
+			}
+		}
+		labelLastClicked = {"axis": focusNode.getAttribute('axis'), "label" : focusNode.innerHTML, "index": Number(focusNode.id.substring(10))};
+		selection.empty();
+	} else if (e.ctrlKey || e.metaKey){ // ctrl or Mac key + click
+		var labelIndex = labelIndexInSearch(this.innerHTML, this.getAttribute('axis'));
+		if (labelIndex > -1){ // if already searched, remove from search items
+			searchItems.splice(labelIndex, 1);
+		} else {
+			searchItems.push({'axis':this.getAttribute('axis'), 'label':this.innerHTML});
+			this.classList.add("searchItem");
+		}
+		labelLastClicked = {"axis": this.getAttribute('axis'), "label" : this.innerHTML, "index": Number(this.id.substring(10))};
+	} else { // standard click
+		clearSearchItems(this.getAttribute('axis'));
+		var labelIndex = labelIndexInSearch(this.innerHTML, this.getAttribute('axis'));
+		if (labelIndex > -1){
+			searchItems.splice(labelIndex, 1);
+		} else {
+			searchItems.push({'axis':this.getAttribute('axis'), 'label':this.innerHTML});
+		}
+		labelLastClicked = {"axis": this.getAttribute('axis'), "label" : this.innerHTML, "index": Number(this.id.substring(10))};
+	}
+	var searchElement = document.getElementById('search_text');
+	searchElement.value = "";
+//	clearSrchBtns();
+	document.getElementById('prev_btn').style.display='';
+	document.getElementById('next_btn').style.display='';
+	document.getElementById('cancel_btn').style.display='';
+	clearLabels();
+	clearSelectionMarks();
+	detailDrawRowClassBarLabels();
+	detailDrawColClassBarLabels();
+	drawRowLabels();
+	drawColLabels();
+	updateSelection();
+	if (isSub){
+		localStorage.setItem('selected', JSON.stringify(searchItems));
+	}
+	if (!isSub){
+		drawRowSelectionMarks();
+		drawColSelectionMarks();
+	}
+}
+
+function clearSearchItems(clickAxis){ // clears the search items on a particular axis
+	var tempSearchItems = searchItems;
+	searchItems = [];
+	for (var i = 0; i < tempSearchItems.length; i++){
+		var tempSearch = tempSearchItems[i]; 
+		if (tempSearch.axis !== clickAxis){
+			searchItems.push(tempSearch);
+		}
+	}
+//	var searchItemsDiv = document.getElementsByClassName('searchItem');
+//	while (searchItemsDiv.length>0){ // clear highlighted labels
+//		var searchItem = searchItemsDiv[0];
+//		searchItem.classList.remove('searchItem');
+//	}
+	var markLabels = document.getElementsByClassName('MarkLabel');
+	while (markLabels.length>0){ // clear tick marks
+		markLabels[0].remove();
+	}
+}
+
+function highlightAllColLabels(){
+	var selectionSize = selectedStop - selectedStart + 1;
+	if ((mode == "RIBBONH" || mode === "RIBBONH_DETAIL") && selectionSize > 1){
+		clearSearchItems("Column");
+		var labels = document.getElementsByClassName("DynamicLabel");
+		for (var i = 0; i < labels.length; i++){
+			var label = labels[i];
+			if (label.getAttribute('axis') == 'Column' && !label.classList.contains('ClassBar')){
+				searchItems.push({'axis':"Column", 'label':label.innerHTML});
+				label.classList.add('searchItem');
+			}
+		}
+	}
+	drawRowSelectionMarks();
+	drawColSelectionMarks();
+}
+
+function highlightAllRowLabels(){
+	var selectionSize = selectedStop - selectedStart + 1;
+	if ((mode == "RIBBONV" || mode === "RIBBONV_DETAIL") && selectionSize > 1){
+		clearSearchItems("Row");
+		var labels = document.getElementsByClassName("DynamicLabel");
+		for (var i = 0; i < labels.length; i++){
+			var label = labels[i];
+			if (label.getAttribute('axis') == 'Row' && !label.classList.contains('ClassBar')){
+				searchItems.push({'axis':"Row", 'label':label.innerHTML});
+				label.classList.add('searchItem');
+			}
+		}
+	}
+	drawRowSelectionMarks();
+	drawColSelectionMarks();
+}
+
+function labelRightClick(e) {
+    e.preventDefault();
+    labelHelpClose();
+    var axis = e.target.getAttribute('axis');
+    var labels = searchItems;
+    labelHelpOpen(axis,e);
+    return false;
+}
+
+function labelIndexInSearch(label,axis){ // basically a Array.contains function, but for searchItems
+	for (var i=0; i < searchItems.length; i++) {
+        if (searchItems[i].label === label && searchItems[i].axis === axis) {
+            return i;
+        }
+    }
+	return -1;
+}
+
+
+function getSearchLabelsByAxis(axis){
+	var labels = [];
+	for (i = 0; i < searchItems.length; i++){
+		if (searchItems[i]["axis"] === axis){
+			labels.push(searchItems[i]["label"])
+		}
+	}
+	return labels;
+}
+
+//draws row classification bars into the texture array ("dataBuffer"). "names"/"colorSchemes" should be array of strings.
+function detailDrawColClassBars(){
+	var classBars = heatMap.getClassifications();
+	var colClassInfo = getClassBarsToDraw("column");
+	var names = colClassInfo["bars"];
+	var colorSchemes = colClassInfo["colors"];
+
+	var rowClassBarWidth = calculateTotalClassBarHeight("row");
+	var fullWidth = detailDataViewWidth + rowClassBarWidth + detailDendroWidth;
+	var mapHeight = detailDataViewHeight;
+	var pos = fullWidth*mapHeight*BYTE_PER_RGBA;
+	for (var i = 0; i < names.length; i++){	//for each column class bar we draw...
+		var currentClassBar = classBars[names[i]];
+		if (currentClassBar.show === 'Y') {
+			var colorMap = heatMap.getColorMapManager().getColorMap(colorSchemes[i]); // assign the proper color scheme...
+			var classBarLength = getCurrentDetDataPerRow() * dataBoxWidth;
+			pos += fullWidth*paddingHeight*BYTE_PER_RGBA; // draw padding between class bars
+			var line = new Uint8Array(new ArrayBuffer(classBarLength * BYTE_PER_RGBA)); // save a copy of the class bar
+			var loc = 0;
+			for (var k = currentCol; k <= currentCol + getCurrentDetDataPerRow() -1; k++) { 
+				var val = currentClassBar.values[k-1];
+				var color = colorMap.getClassificationColor(val);
+				for (var j = 0; j < dataBoxWidth; j++) {
+					line[loc] = color['r'];
+					line[loc + 1] = color['g'];
+					line[loc + 2] = color['b'];
+					line[loc + 3] = color['a'];
+					loc += BYTE_PER_RGBA;
+				}
+			}
+	
+			for (var j = 0; j < currentClassBar.height-paddingHeight; j++){ // draw the class bar into the dataBuffer
+				pos += (rowClassBarWidth + detailDendroWidth + 1)*BYTE_PER_RGBA;
+				for (var k = 0; k < line.length; k++) { 
+					detTexPixels[pos] = line[k];
+					pos++;
+				}
+				pos+=BYTE_PER_RGBA;
+			}
+	  }
+
+	}
+}
+
+function detailDrawColClassBarLabels() {
+	var scale =  detCanvas.clientHeight / (detailDataViewHeight + calculateTotalClassBarHeight("column")+detailDendroHeight);
+	var colClassInfo = getClassBarsToDraw("column");
+	if (colClassInfo != null && colClassInfo.bars.length > 0) {
+		var names = colClassInfo["bars"];
+		var classBars = heatMap.getClassifications();
+		var fontSize = Math.min((classBars[names[0]].height - paddingHeight) * scale, 11);
+		if (fontSize > 7) {
+			var xPos = detCanvas.clientWidth + 3;
+			var yPos = detailDendroHeight*scale;
+			for (var i = names.length-1; i >= 0; i--){	//for each column class bar 
+				var currentClassBar = classBars[names[i]];
+				if (currentClassBar.show === 'Y') {
+					addLabelDiv(labelElement, 'detail_col_class' + i, 'DynamicLabel ClassBar', names[i], xPos, yPos, fontSize, 'F');
+					yPos += (currentClassBar.height * scale);
+				}
+			}	
+		}
+	}
+}
+
+
+//draws row classification bars into the texture array ("dataBuffer"). "names"/"colorSchemes" should be array of strings.
+function detailDrawRowClassBars(){
+	var rowClassInfo = getClassBarsToDraw("row");
+	var names = rowClassInfo["bars"];
+	var colorSchemes = rowClassInfo["colors"];
+	var detailTotalWidth = detailDendroWidth + calculateTotalClassBarHeight("row") + detailDataViewWidth;
+	var offset = ((detailTotalWidth*detailDataViewBoarder/2)+detailDendroWidth) * BYTE_PER_RGBA; // start position of very bottom dendro
+	var mapWidth = detailDataViewWidth;
+	var mapHeight = detailDataViewHeight;
+	var classBars = heatMap.getClassifications();
+	for (var i = 0; i < names.length; i++){ // for each class bar to draw...
+		var currentClassBar = classBars[names[i]];
+		if (currentClassBar.show === 'Y') {
+			var pos = offset; // move past the dendro and the other class bars...
+			var colorMap = heatMap.getColorMapManager().getColorMap(colorSchemes[i]);
+			var classBarLength = currentClassBar.values.length;
+			for (var j = currentRow + getCurrentDetDataPerCol() - 1; j >= currentRow; j--){ // for each row shown in the detail panel
+				var val = currentClassBar.values[j-1];
+				var color = colorMap.getClassificationColor(val);
+				for (var boxRows = 0; boxRows < dataBoxHeight; boxRows++) { // draw this color to the proper height
+					for (var k = 0; k < currentClassBar.height-paddingHeight; k++){ // draw this however thick it needs to be
+						detTexPixels[pos] = color['r'];
+						detTexPixels[pos + 1] = color['g'];
+						detTexPixels[pos + 2] = color['b'];
+						detTexPixels[pos + 3] = color['a'];
+						pos+=BYTE_PER_RGBA;	// 4 bytes per color
+					}
+	
+					// padding between class bars
+					pos+=paddingHeight*BYTE_PER_RGBA;
+					pos+=(mapWidth + detailDendroWidth)*BYTE_PER_RGBA;
+				}
+			}
+			offset+= currentClassBar.height;
+		}
+	}
+}
+
+function detailDrawRowClassBarLabels() {
+	var scale =  detCanvas.clientWidth / (detailDataViewWidth + calculateTotalClassBarHeight("row")+detailDendroWidth);
+	var colClassInfo = getClassBarsToDraw("row");
+	if (colClassInfo != null && colClassInfo.bars.length > 0) {
+		var names = colClassInfo["bars"];
+		var classBars = heatMap.getClassifications();
+		var fontSize = Math.min((classBars[names[0]].height - paddingHeight) * scale, 11);
+		if (fontSize > 7) {
+			var xPos = detailDendroWidth*scale+fontSize + 5;
+			var yPos = detCanvas.clientHeight + 4;;
+			for (var i = names.length-1; i >= 0; i--){	//for each column class bar 
+				var currentClassBar = classBars[names[i]];
+				if (currentClassBar.show === 'Y') {
+					addLabelDiv(labelElement, 'detail_row_class' + i, 'DynamicLabel ClassBar', names[i], xPos, yPos, fontSize, 'T');
+					xPos += (currentClassBar.height * scale);
+				}
+			}
+		}	
+	}
+}
+
+
+/******************************************************
+ *****  DETAIL DENDROGRAM FUNCTIONS START HERE!!! *****
+ ******************************************************/
+
+//Note: stop position passed in is actually one past the last row/column to be displayed.
+
+function buildDetailDendroMatrix(axis, start, stop, heightRatio){
+	var start3NIndex = convertMapIndexTo3NSpace(start);
+	var stop3NIndex = convertMapIndexTo3NSpace(stop);
+	var boxLength, currentIndex, matrixWidth, dendroBars;
+	var dendroInfo = heatMap.getDendrogram()[axis]; // dendro JSON object
+	if (axis =='Column'){ // assign proper axis-specific variables
+		boxLength = dataBoxWidth;
+		matrixWidth = detailDataViewWidth;
+		dendroBars = colDendroBars; // array of the dendro bars
+	} else {
+		boxLength = dataBoxHeight;
+		matrixWidth = detailDataViewHeight;
+		dendroBars = rowDendroBars;
+	}
+	var numNodes = dendroInfo.length;
+	var lastRow = dendroInfo[numNodes-1];
+	var matrix = new Array(normDetailDendroMatrixHeight+1);
+	for (var i = 0; i < normDetailDendroMatrixHeight+1; i++){
+		matrix[i] = new Array(matrixWidth-1);
+	}
+	var topLineArray = new Array(matrixWidth-1); // this array is made to keep track of which bars have vertical lines that extend outside the matrix
+	var maxHeight = Number(lastRow.split(",")[2])/(heightRatio); // this assumes the heightData is ordered from lowest height to highest
+	
+	// check the left and right endpoints of each bar, and see if they are within the bounds.
+	// then check if the bar is in the desired height. 
+	// if it is, draw it in its entirety, otherwise, see if the bar has a vertical connection with any of the bars in view
+	for (var i = 0; i < numNodes; i++){
+		var bar = dendroInfo[i];
+		var tokes = bar.split(",");
+		var leftJsonIndex = Number(tokes[0]);
+		var rightJsonIndex = Number(tokes[1]);
+		var height = Number(tokes[2]);
+		var left3NIndex = convertJsonIndexTo3NSpace(leftJsonIndex); // location in dendroBars space
+		var right3NIndex = convertJsonIndexTo3NSpace(rightJsonIndex);
+		if (right3NIndex < start3NIndex || stop3NIndex < left3NIndex){continue} //if the bar exists outside of the viewport, skip it
+		
+		var leftLoc = convertJsonIndexToDataViewSpace(leftJsonIndex); // Loc is the location in the dendro matrix
+		var rightLoc = convertJsonIndexToDataViewSpace(rightJsonIndex);
+		var normHeight = Math.round(normDetailDendroMatrixHeight*height/maxHeight); // height in matrix
+		var leftEnd = Math.max(leftLoc, 0);
+		var rightEnd = Math.min(rightLoc, matrixWidth-1);
+		if (height > maxHeight){ // if this line is beyond the viewport max height
+			if (start3NIndex < right3NIndex &&  right3NIndex< stop3NIndex && topLineArray[rightLoc] != 1){ // check to see if it will be connecting vertically to a line in the matrix 
+				var drawHeight = normDetailDendroMatrixHeight;
+				while (drawHeight > 0 && matrix[drawHeight][rightLoc] != 1){
+					matrix[drawHeight][rightLoc] = 1;
+					drawHeight--;
+				}
+			}
+			if (start3NIndex < left3NIndex &&  left3NIndex< stop3NIndex && topLineArray[leftLoc] != 1){
+				var drawHeight = normDetailDendroMatrixHeight;
+				while (drawHeight > 0 && matrix[drawHeight][leftLoc] != 1){
+					matrix[drawHeight][leftLoc] = 1;
+					drawHeight--;
+				}
+			}
+			for (var loc = leftEnd; loc < rightEnd; loc++){
+				topLineArray[loc] = 1; // mark that the area covered by this bar can no longer be drawn in  by another, higher level bar
+			}
+		} else {
+			for (var j = leftEnd; j < rightEnd; j++){ // draw horizontal line
+				matrix[normHeight][j] = 1;
+			}
+			var drawHeight = normHeight-1;
+			while (drawHeight > 0 && matrix[drawHeight][leftLoc] != 1 && leftLoc > 0){	// draw left vertical line
+				matrix[drawHeight][leftLoc] = 1;
+				drawHeight--;
+			}
+			drawHeight = normHeight;
+			while (matrix[drawHeight][rightLoc] != 1 && drawHeight > 0 && rightLoc < matrixWidth-1){ // draw right vertical line
+				matrix[drawHeight][rightLoc] = 1;
+				drawHeight--;
+			}
+		}
+	}
+	
+	// fill in any missing leaves but only if the viewport is zoomed in far enough to tell.
+	if (stop - start < 100){
+		var numLeafsDrawn = 0;
+		for (var j in matrix[1]){numLeafsDrawn++}
+		var pos = Math.round(boxLength/2);
+		if (numLeafsDrawn < stop-start){ // have enough lines been drawn?
+			for (var i = 0; i < stop-start; i++){
+				var height = 1;
+				if (matrix[height][pos] != 1){
+					while (height < normDetailDendroMatrixHeight+1){
+						matrix[height][pos] = 1;
+						height++;
+					}
+				}
+				pos += boxLength;
+			}
+		}
+	}
+	
+	return matrix;
+	
+	// HELPER FUNCTIONS
+	function convertMapIndexTo3NSpace(index){
+		return index*pointsPerLeaf - 2;
+	}
+	function convertJsonIndexTo3NSpace(index){
+		if (index < 0){
+			index = 0-index; // make index a positive number to find the leaf
+			return index*pointsPerLeaf - 2;
+		} else {
+			index--; // dendroBars is stored in 3N, so we convert back
+			return Math.round((dendroBars[index].left + dendroBars[index].right)/2); // gets the middle point of the bar
+		}
+	}
+	function convertJsonIndexToDataViewSpace(index){
+		if (index < 0){
+			index = 0-index; // make index a positive number to find the leaf
+			return (index - start)*boxLength+ Math.round(boxLength/2)
+		} else {
+			index--; // dendroBars is stored in 3N, so we convert back
+			var normDistance = (Math.round((dendroBars[index].left+ dendroBars[index].right)/2)-start3NIndex) / (stop3NIndex-start3NIndex); // gets the middle point of the bar
+			return Math.round(normDistance*matrixWidth);
+		}
+	}
+}
+
+function colDendroMatrixCoordToDetailTexturePos(matrixRow,matrixCol){ // convert the matrix coord to the data buffer position (start of the RGBA block)
+	var mapx = matrixCol*getSamplingRatio('row');
+	var mapy = Math.round(matrixRow/normDetailDendroMatrixHeight * columnDendroHeight);
+	var detailTotalWidth = detailDendroWidth + calculateTotalClassBarHeight("row") + detailDataViewWidth;
+	var pos = (detailTotalWidth*(calculateTotalClassBarHeight("column") + detailDataViewHeight))*BYTE_PER_RGBA;
+	pos += (detailDendroWidth + calculateTotalClassBarHeight("row")-1)*BYTE_PER_RGBA;
+	pos += ((mapy)*detailTotalWidth)*BYTE_PER_RGBA + matrixCol*BYTE_PER_RGBA;
+	return pos;
+}
+
+function rowDendroMatrixCoordToDetailTexturePos(matrixRow,matrixCol){ // convert matrix coord to data buffer position (leftmost column of matrix corresponds to the top row of the map)
+	var mapx = detailDataViewHeight - matrixCol-detailDataViewBoarder/2;
+	var mapy = detailDendroWidth - Math.round(matrixRow/normDetailDendroMatrixHeight * detailDendroWidth); // bottom most row of matrix is at the far-right of the map dendrogram 
+	var detailTotalWidth = detailDendroWidth + calculateTotalClassBarHeight("row") + detailDataViewWidth;
+	var pos = (mapx*detailTotalWidth)*BYTE_PER_RGBA + (mapy)*BYTE_PER_RGBA; // pass the empty space (if any) and the border width, to get to the height on the map
+	return pos;
+}
+
+function detailDrawColDendrogram(dataBuffer){
+	var detailTotalWidth = detailDendroWidth + calculateTotalClassBarHeight("row") + detailDataViewWidth;
+	for (var i = 0; i < colDetailDendroMatrix.length; i++){
+		var line = colDetailDendroMatrix[i]; // line = each row of the col dendro matrix
+		for (var j in line){
+			var pos = colDendroMatrixCoordToDetailTexturePos(i,Number(j));
+			if (j > detailDataViewWidth){ // TO DO: find out why some rows in the dendro matrix are longer than they should be
+				continue;
+			}else {
+				dataBuffer[pos] = 3,dataBuffer[pos+1] = 3,dataBuffer[pos+2] = 3,dataBuffer[pos+3] = 255;
+			}
+		}
+	}
+}
+
+function detailDrawRowDendrogram(dataBuffer){
+	for (var i = 0; i <= rowDetailDendroMatrix.length+1; i++){
+		var line = rowDetailDendroMatrix[i]; // line = each row of the col dendro matrix
+		for (var j  in line){
+			var pos = rowDendroMatrixCoordToDetailTexturePos(i,Number(j));
+			if (j > detailDataViewHeight){ // TO DO: find out why some rows in the dendro matrix are longer than they should be
+				continue;
+			} else {
+				dataBuffer[pos] = 3,dataBuffer[pos+1] = 3,dataBuffer[pos+2] = 3,dataBuffer[pos+3] = 255;
+			}
+		}
+	}
+}
+
+function clearDetailDendrograms(){
+	var rowClassWidth = calculateTotalClassBarHeight('row');
+	var detailFullWidth = detailDendroWidth + rowClassWidth  + detailDataViewWidth;
+	var pos = 0;
+	// clear the row dendro pixels
+	for (var i =0; i < detailDataViewHeight*BYTE_PER_RGBA; i++){
+		for (var j = 0; j < detailDendroWidth*BYTE_PER_RGBA; j++){
+			detTexPixels[pos] = undefined;
+			pos++;
+		};
+		pos += ( detailDataViewWidth + rowClassWidth)*BYTE_PER_RGBA;
+	}
+	//clear the column dendro pixels
+	pos = (detailFullWidth) * (detailDataViewHeight + calculateTotalClassBarHeight("column")) * BYTE_PER_RGBA;
+	for (var i =0; i < detailDendroHeight; i++){
+		for (var j = 0; j < detailFullWidth*BYTE_PER_RGBA; j++){
+			detTexPixels[pos] = undefined;
+			pos++;
+		}
+	}
+}
+
+function getSamplingRatio(axis){
+	if (axis == 'row'){
+		switch (mode){
+			case 'RIBBONH': return heatMap.getRowSummaryRatio(MatrixManager.RIBBON_HOR_LEVEL);
+			case 'RIBBONV': return heatMap.getRowSummaryRatio(MatrixManager.RIBBON_VERT_LEVEL);
+			default:        return heatMap.getRowSummaryRatio(MatrixManager.DETAIL_LEVEL);
+		}
+	} else {
+		switch (mode){
+			case 'RIBBONH': return heatMap.getColSummaryRatio(MatrixManager.RIBBON_HOR_LEVEL);
+			case 'RIBBONV': return heatMap.getColSummaryRatio(MatrixManager.RIBBON_VERT_LEVEL);
+			default:        return  heatMap.getColSummaryRatio(MatrixManager.DETAIL_LEVEL);
+		}
+	}
+}
+
+/****************************************************
+ *****  DETAIL DENDROGRAM FUNCTIONS END HERE!!! *****
+ ****************************************************/
+
+
+//WebGL stuff
+
+function detSetupGl() {
+	det_gl = detCanvas.getContext("experimental-webgl", {preserveDrawingBuffer: true});
+	det_gl.viewportWidth = detailDataViewWidth+calculateTotalClassBarHeight("row")+detailDendroWidth;
+	det_gl.viewportHeight = detailDataViewHeight+calculateTotalClassBarHeight("column")+detailDendroHeight;
+	det_gl.clearColor(1, 1, 1, 1);
+
+	var program = det_gl.createProgram();
+	var vertexShader = getDetVertexShader(det_gl);
+	var fragmentShader = getDetFragmentShader(det_gl);
+	det_gl.program = program;
+	det_gl.attachShader(program, vertexShader);
+	det_gl.attachShader(program, fragmentShader);
+	det_gl.linkProgram(program);
+	det_gl.useProgram(program);
+}
+
+
+function getDetVertexShader(theGL) {
+	var source = 'attribute vec2 position;    ' +
+		         'varying vec2 v_texPosition; ' +
+		         'uniform vec2 u_translate;   ' +
+		         'uniform vec2 u_scale;       ' +
+		         'void main () {              ' +
+		         '  vec2 scaledPosition = position * u_scale;               ' +
+		         '  vec2 translatedPosition = scaledPosition + u_translate; ' +
+		         '  gl_Position = vec4(translatedPosition, 0, 1);           ' +
+		         '  v_texPosition = position * 0.5 + 0.5;                   ' +
+		         '}';
+
+
+	var shader = theGL.createShader(theGL.VERTEX_SHADER);
+	theGL.shaderSource(shader, source);
+	theGL.compileShader(shader);
+	if (!theGL.getShaderParameter(shader, theGL.COMPILE_STATUS)) {
+        alert(theGL.getShaderInfoLog(shader));
+    }
+
+	return shader;
+}
+
+
+function getDetFragmentShader(theGL) {
+	var source = 'precision mediump float;        ' +
+		  		 'varying vec2 v_texPosition;     ' +
+ 		 		 'varying float v_boxFlag;        ' +
+ 		 		 'uniform sampler2D u_texture;    ' +
+ 		 		 'uniform vec2 u_box_left_top;    ' +
+ 		 		 'uniform vec2 u_box_right_bottom;' +
+ 		 		 'uniform float u_box_thickness;  ' +
+ 		 		 'uniform vec4 u_box_color;       ' +
+ 		 		 'void main () {                  ' +
+ 		 		 '  vec2 difLeftTop = v_texPosition - u_box_left_top; ' +
+ 		 		 '  vec2 difRightBottom = v_texPosition - u_box_right_bottom; ' +
+ 		 		 '  if (v_texPosition.y >= u_box_left_top.y && v_texPosition.y <= u_box_right_bottom.y) { ' +
+ 		 		 '    if ((difLeftTop.x <= u_box_thickness && difLeftTop.x >= -u_box_thickness) ||  ' +
+ 		 		 '        (difRightBottom.x <= u_box_thickness && difRightBottom.x >= -u_box_thickness)) { ' +
+ 		 		 '      gl_FragColor = u_box_color; ' +
+ 		 		 '    } else { ' +
+ 		 		 '      gl_FragColor = texture2D(u_texture, v_texPosition); ' +
+ 		 		 '    } ' +
+ 		 		 '  } else if (v_texPosition.x >= u_box_left_top.x && v_texPosition.x <= u_box_right_bottom.x) { ' +
+ 		 		 '	  if ((difLeftTop.y <= u_box_thickness && difLeftTop.y >= -u_box_thickness) || ' +
+ 		 		 '	      (difRightBottom.y <= u_box_thickness && difRightBottom.y >= -u_box_thickness)) { ' +
+ 		 		 '	    gl_FragColor = u_box_color; ' +
+ 		 		 '	  } else { ' +
+ 		 		 '	    gl_FragColor = texture2D(u_texture, v_texPosition); ' +
+ 		 		 '	  } ' +
+ 		 		 '	} else { ' +
+ 		 		 '	  gl_FragColor = texture2D(u_texture, v_texPosition); ' +
+ 		 		 '	} ' +
+ 		 		 '}'; 
+
+
+	var shader = theGL.createShader(theGL.FRAGMENT_SHADER);;
+	theGL.shaderSource(shader, source);
+	theGL.compileShader(shader);
+	if (!theGL.getShaderParameter(shader, theGL.COMPILE_STATUS)) {
+        alert(theGL.getShaderInfoLog(shader));
+    }
+
+	return shader;
+}
+
+
+
+function detInitGl () {
+	det_gl.viewport(0, 0, det_gl.viewportWidth, det_gl.viewportHeight);
+	det_gl.clear(det_gl.COLOR_BUFFER_BIT);
+
+	// Vertices
+	var buffer = det_gl.createBuffer();
+	det_gl.buffer = buffer;
+	det_gl.bindBuffer(det_gl.ARRAY_BUFFER, buffer);
+	var vertices = [ -1, -1, 1, -1, 1, 1, -1, -1, -1, 1, 1, 1 ];
+	det_gl.bufferData(det_gl.ARRAY_BUFFER, new Float32Array(vertices), det_gl.STATIC_DRAW);
+	var byte_per_vertex = Float32Array.BYTES_PER_ELEMENT;
+	var component_per_vertex = 2;
+	buffer.numItems = vertices.length / component_per_vertex;
+	var stride = component_per_vertex * byte_per_vertex;
+	var program = det_gl.program;
+	var position = det_gl.getAttribLocation(program, 'position');
+	detUScale = det_gl.getUniformLocation(program, 'u_scale');
+	detUTranslate = det_gl.getUniformLocation(program, 'u_translate');
+	detUBoxLeftTop = det_gl.getUniformLocation(program, 'u_box_left_top');
+	detUBoxRightBottom = det_gl.getUniformLocation(program, 'u_box_right_bottom');
+	detUBoxThickness = det_gl.getUniformLocation(program, 'u_box_thickness');
+	detUBoxColor = det_gl.getUniformLocation(program, 'u_box_color');
+	det_gl.enableVertexAttribArray(position);
+	det_gl.vertexAttribPointer(position, 2, det_gl.FLOAT, false, stride, 0);
+
+	// Texture
+	var texture = det_gl.createTexture();
+	det_gl.bindTexture(det_gl.TEXTURE_2D, texture);
+	det_gl.texParameteri(
+			det_gl.TEXTURE_2D, 
+			det_gl.TEXTURE_WRAP_S, 
+			det_gl.CLAMP_TO_EDGE);
+	det_gl.texParameteri(
+			det_gl.TEXTURE_2D, 
+			det_gl.TEXTURE_WRAP_T, 
+			det_gl.CLAMP_TO_EDGE);
+	det_gl.texParameteri(
+			det_gl.TEXTURE_2D, 
+			det_gl.TEXTURE_MIN_FILTER,
+			det_gl.NEAREST);
+	det_gl.texParameteri(
+			det_gl.TEXTURE_2D, 
+			det_gl.TEXTURE_MAG_FILTER, 
+			det_gl.NEAREST);
+	
+	detTextureParams = {};
+
+	var texWidth = null, texHeight = null, texData;
+	texWidth = detailDataViewWidth + calculateTotalClassBarHeight("row")+detailDendroWidth;
+	texHeight = detailDataViewHeight + calculateTotalClassBarHeight("column")+detailDendroHeight;
+	texData = new ArrayBuffer(texWidth * texHeight * 4);
+	detTexPixels = new Uint8Array(texData);
+	detTextureParams['width'] = texWidth;
+	detTextureParams['height'] = texHeight; 
+}
+
+function toggleGrid(){
+	detailGrid = !detailGrid;
+	drawDetailHeatMap();
+}
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mda_heatmap_viz/static/javascript/MatrixManager.js	Thu Apr 07 14:43:37 2016 -0400
@@ -0,0 +1,568 @@
+//
+// MatrixManager is responsible for retrieving clustered heat map data.  Heat map
+// data is available at different 'zoom' levels - Summary, Ribbon Vertical, Ribbon
+// Horizontal, and Full.  To use this code, create MatrixManger by calling the 
+// MatrixManager function.  The MatrixManager lets you retrieve a HeatmapData object
+// given a heat map name and summary level.  The HeatMapData object has various
+// attributes of the map including the size an number of tiles the map is broken up 
+// into.  getTile() is called on the HeatmapData to get each tile of the data.  Tile
+// retrieval is asynchronous so you need to provide a callback that is triggered when
+// the tile is retrieved.
+//
+
+//Supported map data summary levels.
+MatrixManager.THUMBNAIL_LEVEL = 'tn';
+MatrixManager.SUMMARY_LEVEL = 's';
+MatrixManager.RIBBON_VERT_LEVEL = 'rv';
+MatrixManager.RIBBON_HOR_LEVEL = 'rh';
+MatrixManager.DETAIL_LEVEL = 'd';
+
+MatrixManager.WEB_SOURCE = 'W';
+MatrixManager.FILE_SOURCE = 'F';
+
+MatrixManager.Event_INITIALIZED = 'Init';
+MatrixManager.Event_JSON = 'Json';
+MatrixManager.Event_NEWDATA = 'NewData';
+
+
+//Create a MatrixManager to retrieve heat maps. 
+//Need to specify a mode of heat map data - 
+//web server or local file.
+function MatrixManager(mode){
+	
+	//Main function of the matrix manager - retrieve a heat map object.
+	//mapFile parameter is only used for local file based heat maps.
+	this.getHeatMap = function (heatMapName, updateCallback, mapFile) {
+		return  new HeatMap(heatMapName, updateCallback, mode, mapFile);
+	}	
+};    	
+
+
+//HeatMap Object - holds heat map properties and a tile cache
+//Used to get HeatMapData object.
+//ToDo switch from using heat map name to blob key?
+function HeatMap (heatMapName, updateCallback, mode, chmFile) {
+	//This holds the various zoom levels of data.
+	var datalayers = {};
+	var tileCache = {};
+	var zipFiles = {};
+	var colorMaps = null;
+	var colorMapMgr;
+	var classifications = null;
+	var rowLabels = null;
+	var colLabels = null;
+	var dendrogram = null;
+	var chm = null;
+	var initialized = 0;
+	var eventListeners = [];
+	
+	//Return the number of rows for a given level
+	this.isSaveAllowed = function(){
+		if (mode === "F") {
+			return false;
+		} else {
+			return true;
+		}
+	}
+	
+	//Return the number of rows for a given level
+	this.getNumRows = function(level){
+		return datalayers[level].totalRows;
+	}
+	
+	//Return the number of columns for a given level
+	this.getNumColumns = function(level){
+		return datalayers[level].totalColumns;
+	}
+	
+	//Return the row summary ratio for a given level
+	this.getRowSummaryRatio = function(level){
+		return datalayers[level].rowSummaryRatio;
+	}
+	
+	//Return the column summary ratio for a given level
+	this.getColSummaryRatio = function(level){
+		return datalayers[level].colSummaryRatio;
+	}
+	
+	//Get a data value in a given row / column
+	this.getValue = function(level, row, column) {
+		return datalayers[level].getValue(row,column);
+	}
+	
+	this.saveHeatMapProperties = function () {
+		var success = saveMapProperties("colorMap",JSON.stringify(colorMaps));
+		if (success !== "false") {
+			saveMapProperties("classifications",JSON.stringify(classifications));
+		} 
+		if (success !== "false") {
+			saveMapProperties("dendrogram",JSON.stringify(dendrogram));
+		} 
+		return success;
+	}
+	
+	//This function is used to set a read window for high resolution data layers.
+	//Calling setReadWindow will cause the HeatMap object to retrieve tiles needed
+	//for reading this area if the tiles are not already in the cache.
+    this.setReadWindow = function(level, row, column, numRows, numColumns) {
+  	//Thumb nail and summary level are always kept in the cache.  Don't do fetch for them.
+  	if (level != MatrixManager.THUMBNAIL_LEVEL && level != MatrixManager.SUMMARY_LEVEL)
+  		datalayers[level].setReadWindow(row, column, numRows, numColumns);
+    } 	
+
+	// Retrieve color map Manager for this heat map.
+	this.getColorMapManager = function() {
+		if (initialized != 1)
+			return null;
+		
+		if (colorMapMgr == null ) {
+			colorMapMgr = new ColorMapManager(colorMaps);
+		}
+		return colorMapMgr;
+	}
+	
+	//Retrieve classifications
+	this.getClassifications = function() {
+		return classifications;
+	}
+	
+	this.setClassificationShow = function(classname, value) {
+		classifications[classname].show = value ? 'Y' : 'N';
+	}
+	
+	this.setClassificationPrefs = function(classname, showVal, heightVal) {
+		classifications[classname].show = showVal ? 'Y' : 'N';
+		classifications[classname].height = parseInt(heightVal);
+	}
+	
+	//Get Row Labels
+	this.getRowLabels = function() {
+		return rowLabels;
+	}
+	
+	//Get Column Labels
+	this.getColLabels = function() {
+		return colLabels;
+	}
+	
+	//Get Column Labels
+	this.getChm = function() {
+		return chm; 
+	}
+
+	//Get Column Labels
+	this.getDendrogram = function() {
+		return dendrogram;
+	}
+	
+	this.setRowDendrogramShow = function(value) {
+		dendrogram.row_dendro_show = value;
+	}
+	
+	this.setColDendrogramShow = function(value) {
+		dendrogram.col_dendro_show = value;
+	}
+	
+	this.setRowDendrogramHeight = function(value) {
+		dendrogram.row_dendro_height = value;
+	}
+	
+	this.setColDendrogramHeight = function(value) {
+		dendrogram.col_dendro_height = value;
+	}
+	
+	this.showRowDendrogram = function(layer) {
+		var showDendro = true;
+		if ((dendrogram.row_dendro_show === 'NONE') || (dendrogram.row_dendro_show === 'NA')) {
+			showDendro = false;
+		}
+		if ((layer === 'DETAIL') && (dendrogram.row_dendro_show === 'SUMMARY')) {
+			showDendro = false;
+		}
+		return showDendro;
+	}
+
+	this.showColDendrogram = function(layer) {
+		var showDendro = true;
+		if ((dendrogram.col_dendro_show === 'NONE') || (dendrogram.col_dendro_show === 'NA')) {
+			showDendro = false;
+		}
+		if ((layer === 'DETAIL') && (dendrogram.col_dendro_show === 'SUMMARY')) {
+			showDendro = false;
+		}
+		return showDendro;
+	}
+
+	
+	//Method used to register another callback function for a user that wants to be notifed
+	//of updates to the status of heat map data.
+	this.addEventListener = function(callback) {
+		eventListeners.push(callback);
+	}
+	
+	//Is the heat map ready for business 
+	this.isInitialized = function() {
+		return initialized;
+	}
+	
+	//ToDo: Add methods for getting ordering/dendrogram
+	
+	
+	//************************************************************************************************************
+	//
+	// Internal Heat Map Functions.  Users of the heat map object don't need to use / understand these.
+	//
+	//************************************************************************************************************
+	
+	//Initialization - this code is run once when the map is created.
+	
+	//Add the original update call back to the event listeners list.
+	eventListeners.push(updateCallback);
+	
+	if (mode == MatrixManager.WEB_SOURCE){
+		//mode is web so user server to initialize.
+		
+		//Retrieve  the high-level information about how many data tiles there are at each level.
+		webFetchJson('tilestructure', addDataLayers);
+	
+		//Retrieve the color maps.
+		webFetchJson('colormaps', addColor);
+
+		//Retrieve classification data.
+		webFetchJson('classifications', addClassification);
+		
+		//Retrieve classification data.
+		webFetchJson('rowLabels', addRowLabels);
+		
+		//Retrieve classification data.
+		webFetchJson('colLabels', addColLabels);
+		
+		//Retrieve dendrogram data.
+		webFetchJson('dendrogram', addDendrogram);
+
+		//Retrieve chm data.
+		webFetchJson('chm', addChm);
+	} else {
+		//mode is file so get the json files from the zip file.
+		
+		//First create a dictionary of all the files in the zip.
+		var zipBR = new zip.BlobReader(chmFile);
+		zip.createReader(zipBR, function(reader) {
+			// get all entries from the zip
+			reader.getEntries(function(entries) {
+				for (var i = 0; i < entries.length; i++) {
+					zipFiles[entries[i].filename] = entries[i];
+				}
+				zipFetchJson('tilestructure.json', addDataLayers);	
+				zipFetchJson('colormaps.json', addColor);	
+				zipFetchJson('classifications.json', addClassification);	
+				zipFetchJson('rowLabels.json', addRowLabels);	
+				zipFetchJson('colLabels.json', addColLabels);
+				zipFetchJson('dendrogram.json', addDendrogram);		
+				zipFetchJson('chm.json', addChm);		
+			});
+		}, function(error) {
+			console.log('Zip file read error ' + error);
+		});	
+	}
+	
+	
+	function saveMapProperties(type, jsonData) {
+		var success = "false";
+		var name = "SaveMapProperties?map=" + heatMapName + "&type=" + type;
+		var req = new XMLHttpRequest();
+		req.open("POST", name, false);
+		req.setRequestHeader("Content-Type", "application/json");
+		//req.responseType = "text";
+		req.onreadystatechange = function () {
+			if (req.readyState == req.DONE) {
+				if (req.status != 200) {
+					console.log('Failed in call to save propeties from server: ' + req.status);
+					success = "false";
+				} else {
+					success = req.response;
+				}
+			}
+		};	
+		req.send(jsonData);
+		return success;
+	}
+
+	//  Initialize the data layers once we know the tile structure.
+	//  JSON structure object describing available data layers passed in.
+	function addDataLayers(tileStructure) {
+		//Create heat map data objects for each data level.  All maps should have thumb nail and full level.
+		//Each data layer keeps a pointer to the next lower level data layer.
+      
+		//Thumb nail
+		if (tileStructure.levels.tn !== undefined) {
+			datalayers[MatrixManager.THUMBNAIL_LEVEL] = new HeatMapData(heatMapName, 
+                                                         MatrixManager.THUMBNAIL_LEVEL,
+                                                         tileStructure.levels.tn,
+                                                         null,
+                                                         tileCache,
+                                                         getTile); //special callback for thumb nail.
+			//Kickoff retrieve of thumb nail data tile.
+			datalayers[MatrixManager.THUMBNAIL_LEVEL].setReadWindow(1,1,tileStructure.levels.tn.total_rows,tileStructure.levels.tn.total_cols);
+		}
+      
+
+		//Summary
+		if (tileStructure.levels.s !== undefined) {
+			datalayers[MatrixManager.SUMMARY_LEVEL] = new HeatMapData(heatMapName, 
+                                                       MatrixManager.SUMMARY_LEVEL,
+                                                       tileStructure.levels.s,
+                                                       datalayers[MatrixManager.THUMBNAIL_LEVEL],
+                                                       tileCache,
+                                                       getTile);
+			//Kickoff retrieve of summary data tiles.
+			datalayers[MatrixManager.SUMMARY_LEVEL].setReadWindow(1,1,datalayers[MatrixManager.SUMMARY_LEVEL].totalRows,datalayers[MatrixManager.SUMMARY_LEVEL].totalColumns);
+		} else {			
+			//If no summary level, set the summary to be the thumb nail.
+			datalayers[MatrixManager.SUMMARY_LEVEL] = datalayers[MatrixManager.THUMBNAIL_LEVEL];
+		}
+
+		//Detail level
+		if (tileStructure.levels.d !== undefined) {
+			datalayers[MatrixManager.DETAIL_LEVEL] = new HeatMapData(heatMapName, 
+                                                    MatrixManager.DETAIL_LEVEL,
+                                                    tileStructure.levels.d,
+                                                    datalayers[MatrixManager.SUMMARY_LEVEL],
+                                                    tileCache,
+                                                    getTile);
+		} else {
+			//If no detail layer, set it to summary.
+			datalayers[MatrixManager.DETAIL_LEVEL] = datalayers[MatrixManager.SUMMARY_LEVEL];
+		}
+
+		
+				
+		//Ribbon Vertical
+		if (tileStructure.levels.rv !== undefined) {
+			datalayers[MatrixManager.RIBBON_VERT_LEVEL] = new HeatMapData(heatMapName, 
+	        		                                         MatrixManager.RIBBON_VERT_LEVEL,
+	        		                                         tileStructure.levels.rv,
+	        		                                         datalayers[MatrixManager.SUMMARY_LEVEL],
+	        		                                         tileCache,
+	        		                                         getTile);
+		} else {
+			datalayers[MatrixManager.RIBBON_VERT_LEVEL] = datalayers[MatrixManager.DETAIL_LEVEL];
+		}
+      
+		//Ribbon Horizontal
+		if (tileStructure.levels.rh !== undefined) {
+			datalayers[MatrixManager.RIBBON_HOR_LEVEL] = new HeatMapData(heatMapName, 
+	        		                                         MatrixManager.RIBBON_HOR_LEVEL,
+	        		                                         tileStructure.levels.rh,
+	        		                                         datalayers[MatrixManager.SUMMARY_LEVEL],
+	        		                                         tileCache,
+	        		                                         getTile);
+		} else {
+			datalayers[MatrixManager.RIBBON_HOR_LEVEL] = datalayers[MatrixManager.DETAIL_LEVEL];
+		}
+		
+		sendCallBack(MatrixManager.Event_INITIALIZED);
+	}
+	
+	function addColor(cm) {
+		colorMaps = cm;
+		sendCallBack(MatrixManager.Event_JSON);
+	}
+	
+	
+	function addClassification(cs) {
+		classifications = cs;
+		sendCallBack(MatrixManager.Event_JSON);
+	}
+	
+	function addRowLabels(rl) {
+		rowLabels = rl;
+		sendCallBack(MatrixManager.Event_JSON);
+	}
+	
+	function addColLabels(cl) {
+		colLabels = cl;
+		sendCallBack(MatrixManager.Event_JSON);
+	}
+	
+	function addDendrogram(d) {
+		dendrogram = d;
+		sendCallBack(MatrixManager.Event_JSON);
+	}
+	
+	function addChm(d) {
+		chm = d;
+		sendCallBack(MatrixManager.Event_JSON);
+	}
+	
+	
+	//Call the users call back function to let them know the chm is initialized or updated.
+	function sendCallBack(event, level) {
+		
+		//Initialize event
+		if ((event == MatrixManager.Event_INITIALIZED) || (event == MatrixManager.Event_JSON) ||
+			((event == MatrixManager.Event_NEWDATA) && (level == MatrixManager.THUMBNAIL_LEVEL))) {
+			//Only send initialized status if several conditions are met: need all summary JSON and thumb nail.
+			if ((colorMaps != null) &&
+				(classifications != null) &&
+				(rowLabels != null) &&
+				(colLabels != null) &&
+				(dendrogram != null) &&
+				(chm != null) &&
+				(Object.keys(datalayers).length > 0) &&
+				(tileCache[MatrixManager.THUMBNAIL_LEVEL+".1.1"] != null)) {
+				initialized = 1;
+				sendAllListeners(MatrixManager.Event_INITIALIZED);
+			}
+			//Unlikely, but possible to get init finished after all the summary tiles.  
+			//As a back stop, if we already have the top left summary tile, send a data update event too.
+			if (tileCache[MatrixManager.SUMMARY_LEVEL+".1.1"] != null) {
+				sendAllListeners(MatrixManager.Event_NEWDATA, MatrixManager.SUMMARY_LEVEL);
+			}
+		} else	if ((event == MatrixManager.Event_NEWDATA) && (initialized == 1)) {
+			//Got a new tile, notify drawing code via callback.
+			sendAllListeners(event, level);
+		}
+	}
+	
+	//send to all event listeners
+	function sendAllListeners(event, level){
+		for (var i = 0; i < eventListeners.length; i++) {
+			eventListeners[i](event, level);
+		}
+	}
+	
+	//Fetch a data tile if needed.
+	function getTile(level, tileRow, tileColumn) {
+		var tileName=level + "." + tileRow + "." + tileColumn;
+		if (tileCache.hasOwnProperty(tileName)) {
+			//Already have tile in cache - do nothing.
+			return;
+		}
+
+  	//ToDo: need to limit the number of tiles retrieved.
+  	
+  	//ToDo: need to remove items from the cache if it is maxed out. - don't get rid of thumb nail or summary.
+  	
+
+		if (mode == MatrixManager.WEB_SOURCE) {
+			var name = "GetTile?map=" + heatMapName + "&level=" + level + "&tile=" + tileName;
+			var req = new XMLHttpRequest();
+			req.open("GET", name, true);
+			req.responseType = "arraybuffer";
+			req.onreadystatechange = function () {
+				if (req.readyState == req.DONE) {
+					if (req.status != 200) {
+						console.log('Failed in call to get tile from server: ' + req.status);
+					} else {
+						var arrayData = new Float32Array(req.response);
+						tileCache[tileName] = arrayData;
+						sendCallBack(MatrixManager.Event_NEWDATA, level);
+					}
+				}
+			};	
+			req.send();	
+		} else {
+			//File mode - get tile from zip
+			zipFiles[heatMapName + "/" + level + "/" + tileName + '.bin'].getData(new zip.BlobWriter(), function(blob) {
+				var fr = new FileReader();
+				
+				fr.onload = function(e) {
+			        var arrayBuffer = fr.result;
+			        var far32 = new Float32Array(arrayBuffer);
+			        tileCache[tileName] = far32;
+					sendCallBack(MatrixManager.Event_NEWDATA, level);
+			     }
+			    	  
+			     fr.readAsArrayBuffer(blob);		
+			}, function(current, total) {
+				// onprogress callback
+			});		
+		}
+	};
+	
+	//Helper function to fetch a json file from server.  
+	//Specify which file to get and what funciton to call when it arrives.
+	function webFetchJson(jsonFile, setterFunction) {
+		var req = new XMLHttpRequest();
+		req.open("GET", "GetDescriptor?map=" + heatMapName + "&type=" + jsonFile, true);
+		req.onreadystatechange = function () {
+			if (req.readyState == req.DONE) {
+		        if (req.status != 200) {
+		            console.log('Failed to get json file ' + jsonFile + ' for ' + heatMapName + ' from server: ' + req.status);
+		        } else {
+		        	//Got the result - call appropriate setter.
+		        	setterFunction(JSON.parse(req.response));
+			    }
+			}
+		};
+		req.send();
+	}
+	
+	//Helper function to fetch a json file from zip file.  
+	//Specify which file to get and what funciton to call when it arrives.
+	function zipFetchJson(jsonFile, setterFunction) {
+		zipFiles[heatMapName + "/" + jsonFile].getData(new zip.TextWriter(), function(text) {
+			// got the json, now call the appropriate setter
+			setterFunction(JSON.parse(text));
+		}, function(current, total) {
+			// onprogress callback
+		});
+	}
+	
+};
+
+
+//Internal object for traversing the data at a given zoom level.
+function HeatMapData(heatMapName, level, jsonData, lowerLevel, tileCache, getTile) {	
+	this.totalRows = jsonData.total_rows;
+	this.totalColumns = jsonData.total_cols;
+    var numTileRows = jsonData.tile_rows;
+    var numTileColumns = jsonData.tile_cols;
+    var rowsPerTile = jsonData.rows_per_tile;
+    var colsPerTile = jsonData.cols_per_tile;
+    this.rowSummaryRatio = jsonData.row_summary_ratio;
+    this.colSummaryRatio = jsonData.col_summary_ratio;
+	var rowToLower = (lowerLevel === null ? null : this.totalRows/lowerLevel.totalRows);
+	var colToLower = (lowerLevel === null ? null : this.totalColumns/lowerLevel.totalColumns);
+	
+	//Get a value for a row / column.  If the tile with that value is not available, get the down sampled value from
+	//the lower data level.
+	this.getValue = function(row, column) {
+		//Calculate which tile holds the row / column we are looking for.
+		var tileRow = Math.floor((row-1)/rowsPerTile) + 1;
+		var tileCol = Math.floor((column-1)/colsPerTile) + 1;
+		arrayData = tileCache[level+"."+tileRow+"."+tileCol];
+
+		//If we have the tile, use it.  Otherwise, use a lower resolution tile to provide a value.
+	    if (arrayData != undefined) {
+	    	//for end tiles, the # of columns can be less than the colsPerTile - figure out the correct num columns.
+			var thisTileColsPerRow = tileCol == numTileColumns ? ((this.totalColumns % colsPerTile) == 0 ? colsPerTile : this.totalColumns % colsPerTile) : colsPerTile; 
+			//Tile data is in one long list of numbers.  Calculate which position maps to the row/column we want.
+	    	return arrayData[(row-1)%rowsPerTile * thisTileColsPerRow + (column-1)%colsPerTile];
+	    } else if (lowerLevel != null) {
+	    	return lowerLevel.getValue(Math.floor(row/rowToLower) + 1, Math.floor(column/colToLower) + 1);
+	    } else {
+	    	return 0;
+	    }	
+	};
+
+	// External user of the matix data lets us know where they plan to read.
+	// Pull tiles for that area if we don't already have them.
+    this.setReadWindow = function(row, column, numRows, numColumns) {
+    	var startRowTile = Math.floor(row/rowsPerTile) + 1;
+    	var startColTile = Math.floor(column/colsPerTile) + 1;
+    	var endRowCalc = (row+(numRows-1))/rowsPerTile;
+    	var endColCalc = (column+(numColumns-1))/colsPerTile;
+		var endRowTile = Math.floor(endRowCalc)+(endRowCalc%1 > 0 ? 1 : 0);
+		var endColTile = Math.floor(endColCalc)+(endColCalc%1 > 0 ? 1 : 0);
+    	
+    	for (var i = startRowTile; i <= endRowTile; i++) {
+    		for (var j = startColTile; j <= endColTile; j++) {
+    			if (tileCache[level+"."+i+"."+j] === undefined)
+    				getTile(level, i, j);
+    		}
+    	}
+    }
+
+};
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mda_heatmap_viz/static/javascript/NGCHM_Util.js	Thu Apr 07 14:43:37 2016 -0400
@@ -0,0 +1,34 @@
+/**
+ * General purpose javascript helper funcitons
+ */
+
+//Get a value for a parm passed in the URL.
+function getURLParameter(name) {
+  return decodeURIComponent((new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec(location.search)||[,""])[1].replace(/\+/g, '%20'))||''
+}
+
+/**********************************************************************************
+ * FUNCTION - toTitleCase: The purpose of this function is to change the case of
+ * the first letter of the first word in each sentence passed in.
+ **********************************************************************************/
+function toTitleCase(string)
+{
+    // \u00C0-\u00ff for a happy Latin-1
+    return string.toLowerCase().replace(/_/g, ' ').replace(/\b([a-z\u00C0-\u00ff])/g, function (_, initial) {
+        return initial.toUpperCase();
+    }).replace(/(\s(?:de|a|o|e|da|do|em|ou|[\u00C0-\u00ff]))\b/ig, function (_, match) {
+        return match.toLowerCase();
+    });
+}
+
+/**********************************************************************************
+ * FUNCTION - getStyle: The purpose of this function is to return the style 
+ * property requested for a given screen object.
+ **********************************************************************************/
+function getStyle(x,styleProp){
+    if (x.currentStyle)
+        var y = x.currentStyle[styleProp];
+    else if (window.getComputedStyle)
+        var y = document.defaultView.getComputedStyle(x,null).getPropertyValue(styleProp);
+    return y;
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mda_heatmap_viz/static/javascript/PdfGenerator.js	Thu Apr 07 14:43:37 2016 -0400
@@ -0,0 +1,389 @@
+function openPdfPrefs(e){
+	maxRows = 0;
+	userHelpClose();
+	var prefspanel = document.getElementById('pdfPrefsPanel');
+	//Add prefspanel table to the main preferences DIV and set position and display
+	prefspanel.style.top = e.offsetTop + 15;
+	prefspanel.style.display="inherit";
+	prefspanel.style.left = e.offsetLeft - prefspanel.clientWidth;
+}
+
+/**********************************************************************************
+ * FUNCTION - getPDF: This function is called when the "create pdf" button is pressed.
+ * It will check the checkboxes/radio buttons to see how the PDF is to be created using
+ * the isChecked function. for a full list of jsPDF functions, visit here:
+ * https://mrrio.github.io/jsPDF/doc/symbols/jsPDF.html#setLineCap
+ **********************************************************************************/
+function getPDF(){
+	// canvas elements need to be converted to DataUrl to be loaded into PDF
+	updateSelection(); // redraw the canvases because otherwise they can show up blank
+	var sumImgData = canvas.toDataURL('image/png');
+	var detImgData = detCanvas.toDataURL('image/png');
+	var mapsToShow = isChecked("pdfInputSummaryMap") ? "S" : isChecked("pdfInputDetailMap") ? "D" : "B";
+	var doc = isChecked("pdfInputPortrait") ? new jsPDF("p","pt") :new jsPDF("l","pt"); // landscape or portrait?
+	var pageHeight = doc.internal.pageSize.height;
+	var pageWidth = doc.internal.pageSize.width;
+	
+	doc.setFont("times");
+//	doc.setFont("helvetica");
+	// convert longest label units to actual length (11 is the max font size of the labels)
+	// these will be the bottom and left padding space for the detail Heat Map
+	var allLabels = document.getElementsByClassName("DynamicLabel");
+	var longestRowLabelUnits = 10, longestColLabelUnits = 5;
+	for (var i = 0; i < allLabels.length; i++){ // go through all the labels and find the one that takes the most space
+		var label = allLabels[i];
+		if (label.getAttribute('axis') == "Row"){
+			longestRowLabelUnits = Math.max(doc.getStringUnitWidth(label.innerHTML),longestRowLabelUnits);
+		} else {
+			longestColLabelUnits = Math.max(doc.getStringUnitWidth(label.innerHTML),longestColLabelUnits);
+		}
+	}
+	longestColLabelUnits *= 11;
+	longestRowLabelUnits *= 11;
+	
+	// header
+	var headerCanvas = document.createElement('CANVAS'); // load the MDAnderson logo into a canvas, since you can't load an img directly
+	var headerCtx = headerCanvas.getContext('2d'); 
+	var header = document.getElementsByClassName('mdaServiceHeaderLogo')[0];
+	headerCanvas.height = 85; // logo png's actual dimensions
+	headerCanvas.width = 260;
+	headerCtx.drawImage(header.children[0], 0, 0);
+	var headerData = headerCanvas.toDataURL('image/png');
+	var headerHeight = header.clientHeight + 5;
+	createHeader();
+	
+	// maps
+	var paddingLeft = 5, paddingTop = headerHeight+10; // these are the variables that we will be using repeatedly to place items
+	var sumImgW,sumImgH,detImgW,detImgH;
+	var detImgL = paddingLeft;
+	if (mapsToShow == "S"){
+		sumImgW = pageWidth - 2*paddingLeft, sumImgH = pageHeight - paddingTop - 2*paddingLeft;
+		doc.addImage(sumImgData, 'PNG', paddingLeft, paddingTop, sumImgW,sumImgH);
+	} else if (mapsToShow == "D"){
+		detImgW = pageWidth - 2*paddingLeft - longestRowLabelUnits, detImgH = pageHeight - paddingTop - longestColLabelUnits;
+		doc.addImage(detImgData, 'PNG', paddingLeft, paddingTop, detImgW,detImgH);
+	} else {
+		if (!isChecked("pdfInputPages")){
+			sumImgW = (pageWidth - longestRowLabelUnits - 2*paddingLeft)/2, sumImgH = pageHeight - paddingTop - longestColLabelUnits;
+			detImgW = (pageWidth - longestRowLabelUnits - 2*paddingLeft)/2, detImgH = pageHeight - paddingTop - longestColLabelUnits;
+			detImgL = sumImgW + 2*paddingLeft;
+			doc.addImage(sumImgData, 'PNG', paddingLeft, paddingTop, sumImgW,sumImgH);
+			doc.addImage(detImgData, 'PNG', detImgL, paddingTop, detImgW,detImgH);
+		} else {
+			sumImgW = pageWidth - 2*paddingLeft, sumImgH = pageHeight - paddingTop - 2*paddingLeft;
+			doc.addImage(sumImgData, 'PNG', paddingLeft, paddingTop, sumImgW,sumImgH);
+			doc.addPage();
+			createHeader();
+			detImgW = pageWidth - 2*paddingLeft - longestRowLabelUnits, detImgH = pageHeight - paddingTop - longestColLabelUnits;
+			doc.addImage(detImgData, 'PNG', detImgL, paddingTop, detImgW,detImgH);
+		}
+	}
+
+	// labels
+	var detClient2PdfWRatio = detCanvas.clientWidth/detImgW;  // scale factor to place the labels in their proper locations
+	var detClient2PdfHRatio = detCanvas.clientHeight/detImgH;
+	// row labels and col class bar labels (basically stolen from DetailHeatMapDisplay.js
+	var headerSize = paddingTop;
+	var colHeight = calculateTotalClassBarHeight("column") + detailDendroHeight;
+	if (colHeight > 0) {
+		headerSize += detImgH * (colHeight / (detailDataViewHeight + colHeight));
+	}
+	var skip = (detImgH - headerSize) / dataPerCol;
+	var fontSize = Math.min(skip - 2, 11);
+	doc.setFontSize(fontSize);
+	for (var i = 0; i < allLabels.length; i++){
+		var label = allLabels[i];
+		if (label.getAttribute("axis") == "Row"){
+			doc.text(label.offsetLeft/detClient2PdfWRatio+detImgL, label.offsetTop/detClient2PdfHRatio+paddingTop+fontSize, label.innerHTML, null);
+		} else if (label.getAttribute("axis") == "ColumnClass"){ // change font for class bars
+			var scale =  detImgH / (detailDataViewWidth + calculateTotalClassBarHeight("row")+detailDendroWidth);
+			var colClassInfo = getClassBarsToDraw("column");
+			var names = colClassInfo["bars"];
+			var classBars = heatMap.getClassifications();
+			var tempFontSize = fontSize;
+			fontSize = Math.min((classBars[names[0]].height - paddingHeight) * scale, 11);
+			doc.setFontSize(fontSize);
+			doc.text(label.offsetLeft/detClient2PdfWRatio+detImgL, label.offsetTop/detClient2PdfHRatio+paddingTop+fontSize/2, label.innerHTML, null);
+			fontSize = tempFontSize
+			doc.setFontSize(fontSize);
+		}
+	}
+	
+	// col labels and row class bar labels
+	headerSize = 0;
+	var rowHeight = calculateTotalClassBarHeight("row") + detailDendroWidth;
+	if (rowHeight > 0) {
+		headerSize = detImgW * (rowHeight / (detailDataViewWidth + rowHeight));
+	}
+	skip = (detImgW - headerSize) / dataPerRow;
+	fontSize = Math.min(skip - 2, 11);
+	doc.setFontSize(fontSize);
+	for (var i = 0; i < allLabels.length; i++){
+		var label = allLabels[i];
+		if (label.getAttribute("axis") == "Column"){
+			doc.text(label.offsetLeft/detClient2PdfWRatio-fontSize+detImgL, label.offsetTop/detClient2PdfHRatio+paddingTop, label.innerHTML, null, 270);
+		} else if (label.getAttribute("axis") == "RowClass"){
+			var scale =  detImgW / (detailDataViewWidth + calculateTotalClassBarHeight("row")+detailDendroWidth);
+			var colClassInfo = getClassBarsToDraw("row");
+			var names = colClassInfo["bars"];
+			var classBars = heatMap.getClassifications();
+			var tempFontSize = fontSize;
+			fontSize = Math.min((classBars[names[0]].height - paddingHeight) * scale, 11);
+			doc.setFontSize(fontSize);
+			doc.text(label.offsetLeft/detClient2PdfWRatio-fontSize/2+detImgL, label.offsetTop/detClient2PdfHRatio+paddingTop, label.innerHTML, null, 270);
+			fontSize = tempFontSize
+			doc.setFontSize(fontSize);
+		}
+	}
+	 
+	// class bar legends
+	var classBars = heatMap.getClassifications();
+	var classBarHeaderSize = 20; // these are font sizes
+	var classBarTitleSize = 15;
+	var classBarLegendTextSize = 10;
+	var classBarFigureW = 150; // figure dimensions, unless discrete with 15+ categories
+	var classBarFigureH = 150;
+	var condenseClassBars = isChecked('pdfInputCondensed');
+	paddingLeft = 5, paddingTop = headerHeight+classBarHeaderSize + 5; // reset the top and left coordinates
+	
+	// row
+	if (isChecked('pdfInputRow')){
+		doc.addPage();
+		createHeader();
+		doc.setFontSize(classBarHeaderSize);
+		doc.text(10, paddingTop, "Row Covariate Bar Legends:" , null);
+		var rowClassInfo = getClassBarsToDraw("row");
+		var names = rowClassInfo["bars"];
+		var colorSchemes = rowClassInfo["colors"];
+		var leftOff=10, topOff = paddingTop + classBarTitleSize;
+		
+		for (var i = 0; i < names.length; i++){ // for each class bar to draw...
+			doc.setFontSize(classBarTitleSize);
+			var currentClassBar = classBars[names[i]];
+			var colorMap = heatMap.getColorMapManager().getColorMap(colorSchemes[i]);
+			if (currentClassBar.show === 'Y') { // place the figure if it's shown
+				if (colorMap.getType() == "discrete"){
+					getBarGraphForDiscreteClassBar(currentClassBar,colorMap,names[i]);
+				} else {
+					getBarGraphForContinuousClassBar(currentClassBar,colorMap,names[i]);
+				}
+			}
+		}
+	}
+	
+	// column
+	if (isChecked('pdfInputColumn')){
+		doc.addPage();
+		createHeader();
+		doc.setFontSize(classBarHeaderSize);
+		doc.text(10, paddingTop, "Column Covariate Bar Legends:" , null);
+		var colClassInfo = getClassBarsToDraw("column");
+		var names = colClassInfo["bars"];
+		var colorSchemes = colClassInfo["colors"];
+		var leftOff=10, topOff = paddingTop + classBarTitleSize;
+		
+		for (var i = 0; i < names.length; i++){ // for each class bar to draw...
+			doc.setFontSize(classBarTitleSize);
+			var currentClassBar = classBars[names[i]];
+			var colorMap = heatMap.getColorMapManager().getColorMap(colorSchemes[i]);
+			if (currentClassBar.show === 'Y') {
+				if (colorMap.getType() == "discrete"){
+					getBarGraphForDiscreteClassBar(currentClassBar,colorMap,names[i]);
+				} else {
+					getBarGraphForContinuousClassBar(currentClassBar,colorMap,names[i]);
+				}
+			}
+		}
+	}
+	 
+	// TODO: in case there is an empty page after the class bar legends, delete it
+	
+	
+	doc.save( heatMap.getChm().name + '.pdf');
+	
+	
+	//==================//
+	// HELPER FUNCTIONS //
+	//==================//
+	
+	// makes the MDAnderson logo, the HM name, and the red divider line at the top of each page
+	function createHeader() {
+		doc.addImage(headerData, 'PNG',5,5,header.clientWidth,header.clientHeight);
+		doc.setFontSize(20);
+		doc.text(pageWidth/2 - doc.getStringUnitWidth(heatMap.getChm().name)*20/2, headerHeight, heatMap.getChm().name, null);
+		doc.setFillColor(255,0,0);
+		doc.setDrawColor(255,0,0);
+		doc.rect(5, header.clientHeight+10, pageWidth-10, 2, "FD");
+	}
+	
+	/**********************************************************************************
+	 * FUNCTION - getBarGraphForContinousClassBar: places the classBar legend using the
+	 * variables leftOff and topOff, which are updated after every classBar legend.
+	 * inputs: classBar object, colorMap object, and string for name
+	 **********************************************************************************/
+	function getBarGraphForContinuousClassBar(classBar, colorMap,name){
+		doc.text(leftOff, topOff , name, null);
+		var thresholds = colorMap.getContinuousThresholdKeys();
+		var numThresholds = thresholds.length-1; // the last threshold repeats for some reason :\
+		var barHeight = !condenseClassBars ? classBarFigureH/(thresholds.length) : 10;		
+		// get the number N in each threshold
+		var counts = {}, maxCount = 0, maxLabelLength = doc.getStringUnitWidth("Missing Value")*classBarLegendTextSize;
+		// get the continuous thresholds and find the counts for each bucket
+		for(var i = 0; i < classBar.values.length; i++) {
+		    var num = classBar.values[i];
+		    for (var k = 0; k < thresholds.length; k++){
+				var thresh = thresholds[k];
+				if (k == 0 && num <thresholds[k]){
+					counts[thresh] = counts[thresh] ? counts[thresh]+1 : 1;
+				} else if (k == thresholds.length-1 && num > thresholds[thresholds.length-1]){
+					counts[thresh] = counts[thresh] ? counts[thresh]+1 : 1;
+				} else if (num <= thresh){
+					counts[thresh] = counts[thresh] ? counts[thresh]+1 : 1;
+					break;
+				}
+			}
+		}
+		// find the longest label length
+		for (var val in counts){
+			maxCount = Math.max(maxCount, counts[val]);
+			maxLabelLength = Math.max(maxLabelLength, doc.getStringUnitWidth(val.length)*classBarLegendTextSize);
+		}
+		
+		var bartop = topOff+5; // top location of first bar
+		var missingCount = classBar.values.length; // start at total number of labels and work down
+		for (var j = 0; j < thresholds.length-1; j++){
+			var rgb = colorMap.getClassificationColor(thresholds[j]);
+			doc.setFillColor(rgb.r,rgb.g,rgb.b);
+			doc.setDrawColor(0,0,0);
+			if (condenseClassBars){ // square
+				var barW = 10;
+				doc.rect(leftOff, bartop, barW, barHeight, "FD"); // make the square
+				doc.setFontSize(classBarLegendTextSize);
+				doc.text(leftOff +barW + 5, bartop + classBarLegendTextSize, thresholds[j].toString() + "   " + "n = " + counts[thresholds[j]] , null);
+			} else { // histogram
+				var barW = counts[thresholds[j]]/maxCount*classBarFigureW;
+				doc.rect(leftOff + maxLabelLength, bartop, barW, barHeight, "FD"); // make the histo bar
+				doc.setFontSize(classBarLegendTextSize);
+				doc.text(leftOff + maxLabelLength - doc.getStringUnitWidth(thresholds[j].toString())*classBarLegendTextSize - 4, bartop + classBarLegendTextSize, thresholds[j].toString() , null);
+				doc.text(leftOff + maxLabelLength +barW + 5, bartop + classBarLegendTextSize, "n = " + counts[thresholds[j]] , null);
+			}
+			missingCount -= counts[thresholds[j]]; 
+			bartop+=barHeight; // adjust top position for the next bar
+		}
+		var rgb = colorMap.getClassificationColor("Missing Value");
+		doc.setFillColor(rgb.r,rgb.g,rgb.b);
+		doc.setDrawColor(0,0,0);
+		if (condenseClassBars){
+			var barW = 10;
+			doc.rect(leftOff, bartop, barW, barHeight, "FD");
+			doc.setFontSize(classBarLegendTextSize);
+			doc.text(leftOff +barW + 5, bartop + classBarLegendTextSize, "Missing Value n = " + missingCount , null);
+		} else {
+			var barW = missingCount/maxCount*classBarFigureW;
+			doc.rect(leftOff + maxLabelLength, bartop, barW, barHeight, "FD");
+			doc.setFontSize(classBarLegendTextSize);
+			doc.text(leftOff + maxLabelLength - doc.getStringUnitWidth("Missing Value")*classBarLegendTextSize - 4, bartop + classBarLegendTextSize, "Missing Value" , null);
+			doc.text(leftOff + maxLabelLength +barW + 5, bartop + classBarLegendTextSize, "n = " + missingCount , null);
+		}
+		// adjust the location for the next class bar figure
+		leftOff+= classBarFigureW + maxLabelLength + 50; 
+		if (leftOff + classBarFigureW > pageWidth){ // if we'll go off the width of the page...
+			leftOff = 10; // ...reinitialize the left side
+			topOff += classBarFigureH + classBarHeaderSize;
+			classBarFigureH = 150; // return figure height to original value in case it got changed in the current row
+			if (topOff + classBarFigureH > pageHeight){ // if we'll go off the bottom of the page...
+				doc.addPage();
+				createHeader(); // ...create a new page and reinitialize the top
+				topOff = paddingTop + 5;
+			}
+		}
+	}
+
+
+	/**********************************************************************************
+	 * FUNCTION - getBarGraphForDiscreteClassBar: places the classBar legend using the
+	 * variables leftOff and topOff, which are updated after every classBar legend.
+	 * inputs: classBar object, colorMap object, and string for name
+	 **********************************************************************************/
+	function getBarGraphForDiscreteClassBar(classBar, colorMap,name){
+		doc.text(leftOff, topOff , name, null);
+		var thresholds = colorMap.getThresholds();
+		var barHeight = !condenseClassBars ? classBarFigureH/(thresholds.length+1) : 10;
+		var counts = {}, maxCount = 0, maxLabelLength = doc.getStringUnitWidth("Missing Value")*classBarLegendTextSize;
+		// get the number N in each threshold
+		for(var i = 0; i< classBar.values.length; i++) {
+		    var num = classBar.values[i];
+		    counts[num] = counts[num] ? counts[num]+1 : 1;
+		}
+		for (var val in counts){
+			maxCount = Math.max(maxCount, counts[val]);
+			maxLabelLength = Math.max(maxLabelLength, doc.getStringUnitWidth(val.length)*classBarLegendTextSize);
+		}
+			
+		var bartop = topOff+5;
+		// NOTE: missingCount will contain all elements that are not accounted for in the thresholds
+		// ie: thresholds = [type1, type2, type3], typeX will get included in the missingCount
+		var missingCount = classBar.values.length;
+		// draw the bars
+		for (var j = 0; j < thresholds.length; j++){ // make a gradient stop (and also a bucket for continuous)
+			var rgb = colorMap.getClassificationColor(thresholds[j]);
+			doc.setFillColor(rgb.r,rgb.g,rgb.b);
+			doc.setDrawColor(0,0,0);
+			if (condenseClassBars){
+				var barW = 10;
+				doc.rect(leftOff, bartop, barW, barHeight, "FD");
+				doc.setFontSize(classBarLegendTextSize);
+				doc.text(leftOff +barW + 5, bartop + classBarLegendTextSize, thresholds[j].toString() + "   " + "n = " + counts[thresholds[j]] , null);
+			} else {
+				var barW = counts[thresholds[j]]/maxCount*classBarFigureW;
+				doc.rect(leftOff + maxLabelLength, bartop, barW, barHeight, "FD");
+				doc.setFontSize(classBarLegendTextSize);
+				doc.text(leftOff + maxLabelLength - doc.getStringUnitWidth(thresholds[j].toString())*classBarLegendTextSize - 4, bartop + barHeight/2, thresholds[j].toString() , null);
+				doc.text(leftOff + maxLabelLength +barW + 5, bartop + barHeight/2, "n = " + counts[thresholds[j]] , null);
+			}
+			
+			missingCount -= counts[thresholds[j]];
+			bartop+=barHeight;
+		}
+			
+		var rgb = colorMap.getClassificationColor("Missing Value");
+		doc.setFillColor(rgb.r,rgb.g,rgb.b);
+		doc.setDrawColor(0,0,0);
+		if (condenseClassBars){
+			var barW = 10;
+			doc.rect(leftOff, bartop, barW, barHeight, "FD");
+			doc.setFontSize(classBarLegendTextSize);
+			doc.text(leftOff +barW + 5, bartop + classBarLegendTextSize, "Missing Value n = " + missingCount , null);
+		} else {
+			var barW = missingCount/maxCount*classBarFigureW;
+			doc.rect(leftOff + maxLabelLength, bartop, barW, barHeight, "FD");
+			doc.setFontSize(classBarLegendTextSize);
+			doc.text(leftOff + maxLabelLength - doc.getStringUnitWidth("Missing Value")*classBarLegendTextSize - 4, bartop + barHeight/2, "Missing Value" , null);
+			doc.text(leftOff + maxLabelLength +barW + 5, bartop + barHeight/2, "n = " + missingCount , null);
+		}
+		
+		if (thresholds.length > 15){ // in case a discrete classbar has over 15 categories, make the topOff increment bigger
+			classBarFigureH = (1+thresholds.length)*10;
+		}
+		leftOff+= classBarFigureW + maxLabelLength + 50;
+		if (leftOff + classBarFigureW > pageWidth){ // if the next class bar figure will go beyond the width of the page...
+			leftOff = 10; // ...reset leftOff...
+			topOff += classBarFigureH+classBarHeaderSize; // ... and move the next figure to the line below
+			classBarFigureH = 150; // return class bar height to original value in case it got changed in this row
+			if (topOff + classBarFigureH > pageHeight){ // if the next class bar goes off the page vertically...
+				doc.addPage(); // ... make a new page and reset topOff
+				createHeader();
+				topOff = paddingTop + 10;
+			}
+		}
+	}
+	function isChecked(el){
+		if(document.getElementById(el))
+		return document.getElementById(el).checked;
+	}
+}
+
+function pdfCancelButton(){
+	var prefspanel = document.getElementById('pdfPrefsPanel');
+	prefspanel.style.display = "none";
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mda_heatmap_viz/static/javascript/SelectionManager.js	Thu Apr 07 14:43:37 2016 -0400
@@ -0,0 +1,449 @@
+/**
+ * This code is responsible for handling changes in position of selected heat map region.
+ * It handles mouse, keyboard, and button events that change the position of the selected
+ * region.  It also tracks whether the display is in a single window or split into two
+ * separate windows.  If in separate windows, local storage events are used to communicate
+ * changes between the two windows.  
+ */
+
+//Globals that provide information about heat map position selection.
+
+mode = null;          // Set to normal or ribbon vertical or ribbon horizontal 
+currentRow=null;      // Top row of current selected position
+currentCol=null;      // Left column of the current selected position
+dataPerRow=null;      // How many rows are included in the current selection
+dataPerCol=null;      // How many columns in the current selection
+selectedStart=0;      // If dendrogram selection is used to limit ribbon view - which position to start selection.
+selectedStop=0;       // If dendrogram selection is used to limit ribbon view - which position is last of selection.
+var searchItems=[];   // Valid labels found from a user search
+
+                      //isSub will be set to true if windows are split and this is the child.
+isSub = getURLParameter('sub') == 'true';  
+hasSub = false;       //hasSub set to true if windows are split and this is the parent.
+
+
+/* This routine is called when the selected row / column is changed.
+ * It is assumed that the caller modified currentRow, currentCol, dataPerRow,
+ * and dataPerCol as desired. This method does redrawing and notification as necessary.  
+ */
+function updateSelection() {
+	if (!isSub) {
+		//We have the summary heat map so redraw the yellow selection box.
+		drawLeftCanvasBox();
+	} 
+	if (!hasSub) {
+		// Redraw based on mode type and selection. 
+		heatMap.setReadWindow(getLevelFromMode(MatrixManager.DETAIL_LEVEL),getCurrentDetRow(),getCurrentDetCol(),getCurrentDetDataPerCol(),getCurrentDetDataPerRow());
+		drawDetailHeatMap();
+	} 
+	
+ 	//If summary and detail as split into two browsers.  Communicate the selection change
+	//to the other browser.
+	if (isSub || hasSub) {
+		localStorage.removeItem('event');
+		localStorage.setItem('currentRow', '' + currentRow);
+		localStorage.setItem('currentCol', '' + currentCol);
+		localStorage.setItem('dataPerRow', '' + dataPerRow);
+		localStorage.setItem('dataPerCol', '' + dataPerCol);
+		localStorage.setItem('selectedStart', '' + selectedStart);
+		localStorage.setItem('selectedStop', '' + selectedStop);
+		localStorage.setItem('mode', mode);
+		localStorage.setItem('selected', JSON.stringify(searchItems));
+		localStorage.setItem('event', 'changePosition');
+	}		
+}
+
+function changeMode(newMode) {
+	
+	if (!hasSub) {
+		if (newMode == 'RIBBONH')
+			detailHRibbon();
+		if (newMode == 'RIBBONV')
+			detailVRibbon();
+		if (newMode == 'NORMAL')
+			detailNormal();
+	} else {
+		localStorage.removeItem('event');
+		localStorage.setItem('selectedStart', '' + selectedStart);
+		localStorage.setItem('selectedStop', '' + selectedStop);
+		localStorage.setItem('mode', newMode);
+		localStorage.setItem('event', 'changeMode');
+	}
+}
+
+/* Handle mouse scroll wheel events to zoom in / out.
+ */
+function handleScroll(evt) {
+	evt.preventDefault();
+	if (evt.wheelDelta < 0 || evt.deltaY > 0 || evt.scale < 1) { //Zoom out
+		if (!hasSub)
+			detailDataZoomOut();
+		else {
+			localStorage.removeItem('event');
+			localStorage.setItem('event', 'zoomOut' )
+		}
+	} else { // Zoom in
+		if (!hasSub)
+			detailDataZoomIn();
+		else {
+			localStorage.removeItem('event');
+			localStorage.setItem('event', 'zoomIn' )
+		}
+	}	
+	return false;
+} 		
+
+
+function keyNavigate(e){
+	userHelpClose();
+    clearTimeout(detailPoint);
+	switch(e.keyCode){ // prevent default added redundantly to each case so that other key inputs won't get ignored
+		case 37: // left key 
+			if (document.activeElement.id !== "search_text"){
+				e.preventDefault();
+				if (e.shiftKey){currentCol -= dataPerRow;} 
+				else {currentCol--;}
+			}
+			break;
+		case 38: // up key
+			if (document.activeElement.id !== "search_text"){
+				e.preventDefault();
+				if (e.shiftKey){currentRow -= dataPerCol;} 
+				else {currentRow--;}
+			}
+			break;
+		case 39: // right key
+			if (document.activeElement.id !== "search_text"){
+				e.preventDefault();
+				if (e.shiftKey){currentCol += dataPerRow;} 
+				else {currentCol++;}
+			}
+			break;
+		case 40: // down key
+			if (document.activeElement.id !== "search_text"){
+				e.preventDefault();
+				if (e.shiftKey){currentRow += dataPerCol;} 
+				else {currentRow++;}
+			}
+			break;
+		case 33: // page up
+			e.preventDefault();
+			if (e.shiftKey){
+				var newMode;
+				clearDendroSelection();
+				switch(mode){
+					case "RIBBONV": newMode = 'RIBBONH'; break;
+					case "RIBBONH": newMode = 'NORMAL'; break;
+					default: newMode = mode;break;
+				}
+				changeMode(newMode);
+			} else {
+				detailDataZoomIn();;
+			}
+			break;
+		case 34: // page down 
+			e.preventDefault();
+			if (e.shiftKey){
+				var newMode;
+				clearDendroSelection();
+				switch(mode){
+					case "NORMAL": newMode = 'RIBBONH'; break;
+					case "RIBBONH": newMode = 'RIBBONV'; break;
+					default: newMode = mode;break;
+				}
+				changeMode(newMode);
+			} else {
+				detailDataZoomOut();
+			}
+			break;
+		case 191: // "divide key" /
+			detailSplit();
+			break;
+		default:
+			return;
+	}
+	
+	checkRow();
+	checkColumn();
+    
+    updateSelection();
+}
+
+/* Local storage is used to communicate between two browser windows when the display is split. Set
+ * up an event to be notified when contents of local storage are modified.
+ */ 
+function setupLocalStorage () {
+	window.addEventListener('storage', function (evt) {
+		console.log('localstorage event ' + evt.key);
+		if (evt.key == 'event') {
+			handleLocalStorageEvent(evt);
+		} 
+	}, false);
+}
+
+//When the detail pane is in a separate window, local storage is used to send it updates from 
+//clicks in the summary view.
+function handleLocalStorageEvent(evt) {
+	if (evt.newValue == null)
+		return;
+	
+	var type = localStorage.getItem('event');
+
+	if (type == 'changePosition') {
+		currentRow = Number(localStorage.getItem('currentRow'));
+		currentCol = Number(localStorage.getItem('currentCol'));
+		dataPerRow = Number(localStorage.getItem('dataPerRow'));
+		dataPerCol = Number(localStorage.getItem('dataPerCol'));
+		selectedStart = Number(localStorage.getItem('selectedStart'));
+		selectedStop = Number(localStorage.getItem('selectedStop'));
+		if (mode != localStorage.getItem('mode') && selectedStart == 0 && selectedStop == 0){
+			clearDendroSelection();
+		}
+		mode = localStorage.getItem('mode');
+		if (hasSub) {
+			searchItems = JSON.parse(localStorage.getItem('selected'));
+			// Redraw the yellow selection box.
+			drawLeftCanvasBox ();
+		} 
+		if (isSub) {
+			// Redraw detail view based on selection. 
+			heatMap.setReadWindow(getLevelFromMode(MatrixManager.DETAIL_LEVEL),getCurrentDetRow(),getCurrentDetCol(),getCurrentDetDataPerCol(),getCurrentDetDataPerRow());
+			drawDetailHeatMap();
+		} 
+	} else if ((type == 'zoomIn') && (isSub)) {
+		detailDataZoomIn();
+	} else if ((type == 'zoomOut') && (isSub)) {
+		detailDataZoomOut();
+	} else if ((type == 'changeMode') && (isSub))	{
+		clearDendroSelection();
+		var newMode = localStorage.getItem('mode');
+		selectedStart = Number(localStorage.getItem('selectedStart'));
+		selectedStop = Number(localStorage.getItem('selectedStop'));
+		if (newMode == 'RIBBONH')
+			detailHRibbon();
+		if (newMode == 'RIBBONV')
+			detailVRibbon();
+		if (newMode == 'NORMAL')
+			detailNormal();		
+	} else if ((type == 'join') && hasSub) {
+		hasSub=false;
+		detailJoin();
+	}
+}
+
+//If a second detail browser window is launched, use local storage when first setting
+//up the detail chm to get current mode and selection settings.
+function initFromLocalStorage() {
+	currentRow = Number(localStorage.getItem('currentRow'));
+	currentCol = Number(localStorage.getItem('currentCol'));
+	dataPerRow = Number(localStorage.getItem('dataPerRow'));
+	dataPerCol = Number(localStorage.getItem('dataPerCol'));
+	selectedStart = Number(localStorage.getItem('selectedStart'));
+	selectedStop = Number(localStorage.getItem('selectedStop'));
+	searchItems = JSON.parse(localStorage.getItem('selected'));
+	mode = localStorage.getItem('mode');
+	buildDendroMatrix(heatMap.getDendrogram(),'Column');
+	buildDendroMatrix(heatMap.getDendrogram(),'Row');
+
+	dataBoxHeight = (DETAIL_SIZE_NORMAL_MODE-detailDataViewBoarder)/dataPerCol;
+	dataBoxWidth = (DETAIL_SIZE_NORMAL_MODE-detailDataViewBoarder)/dataPerRow;
+	
+	if (mode == 'RIBBONH')
+		detailHRibbon();
+	if (mode == 'RIBBONV')
+		detailVRibbon();
+	if (mode == 'NORMAL')
+		detailNormal();		
+}
+
+
+
+//Called when a separate detail map window is joined back into the main chm browser window.
+function rejoinNotice() {
+	localStorage.removeItem('event');
+	localStorage.setItem('event', 'join');	
+}
+
+/**********************************************************************************
+ * FUNCTION - getLevelFromMode: This function returns the level that is associated
+ * with a given mode.  A level is passed in from either the summary or detail display
+ * as a default value and returned if the mode is not one of the Ribbon modes.
+ **********************************************************************************/
+function getLevelFromMode(lvl) {
+	if (mode == 'RIBBONV') {
+		return MatrixManager.RIBBON_VERT_LEVEL;
+	} else if (mode == 'RIBBONH') {
+		return MatrixManager.RIBBON_HOR_LEVEL;
+	} else {
+		return lvl;
+	} 
+}
+
+/**********************************************************************************
+ * FUNCTIONS - checkRow(and Col): This function makes sure the currentRow/Col setting 
+ * is valid and adjusts that value into the viewing pane if it is not. It is called
+ * just prior to calling UpdateSelection().
+ **********************************************************************************/
+function checkRow() {
+    //Set column to one if off the row boundary when in ribbon vert view
+	if ((currentRow < 1) || ((mode == 'RIBBONV') && (selectedStart==0))) currentRow = 1;
+	if (((mode == 'RIBBONV') || (mode == 'RIBBONV_DETAIL')) && (selectedStart != 0)) currentRow = selectedStart;
+	//Check row against detail boundaries
+	var numRows = heatMap.getNumRows(MatrixManager.DETAIL_LEVEL);
+	if (currentRow > ((numRows + 1) - dataPerCol)) currentRow = (numRows + 1) - dataPerCol;
+}
+
+function checkColumn() {
+    //Set column to one if off the column boundary when in ribbon horiz view
+    if ((currentCol < 1) || ((mode == 'RIBBONH') && selectedStart==0)) currentCol = 1;
+    if (((mode == 'RIBBONH') || (mode=='RIBBONH_DETAIL')) && selectedStart!= 0) currentCol = selectedStart;
+    //Check column against detail boundaries
+    var numCols = heatMap.getNumColumns(MatrixManager.DETAIL_LEVEL);
+    if (currentCol > ((numCols + 1) - dataPerRow)) currentCol = (numCols + 1) - dataPerRow;
+}
+
+/**********************************************************************************
+ * FUNCTIONS - setCurrentRow(Col)FromSum: These function perform the conversion 
+ * of currentRow and currentCol coordinates from summary to detail.  This is done 
+ * so that the proper row/col location is set on the detail pane when a user clicks 
+ * in the summary pane. The heatmap row/col summary ratios (ratio of detail to summary) 
+ * are used to calculate the proper detail coordinates.  
+ **********************************************************************************/
+function setCurrentRowFromSum(sumRow) {
+	// Up scale current summary row to detail equivalent
+	var rowSummaryRatio = heatMap.getRowSummaryRatio(MatrixManager.SUMMARY_LEVEL);
+	if (rowSummaryRatio > 1) {
+		currentRow = (sumRow*rowSummaryRatio);
+	} else {
+		currentRow = sumRow;
+	}
+}
+function setCurrentColFromSum(sumCol) {
+	var colSummaryRatio = heatMap.getColSummaryRatio(MatrixManager.SUMMARY_LEVEL);
+	if (colSummaryRatio > 1) {
+		currentCol = (sumCol*colSummaryRatio);
+	} else {
+		currentCol = sumCol;
+	}
+}
+
+/**********************************************************************************
+ * FUNCTIONS - getCurrentSumRow(): These functions perform the conversion of 
+ * currentRow and currentCol coordinates from detail to summary.  This is done 
+ * so that the  proper row/col location is set on the summary pane when a user clicks 
+ * in the detail pane. This is used when the leftCanvasBox is drawn. The heat map 
+ * row/col summary ratios (ratio of detail to summary) are used to  calculate the 
+ * proper detail coordinates.
+ **********************************************************************************/
+function getCurrentSumRow() {
+	var currRow = currentRow;
+	// Convert selected current row value to Summary level
+	var rowSummaryRatio = heatMap.getRowSummaryRatio(MatrixManager.SUMMARY_LEVEL);
+	return  Math.round(currRow/rowSummaryRatio);
+}
+//Follow similar methodology for Column as is used in above row based function
+function getCurrentSumCol() {
+	var currCol = currentCol;
+	var colSummaryRatio = heatMap.getColSummaryRatio(MatrixManager.SUMMARY_LEVEL);
+	return  Math.round(currCol/colSummaryRatio);
+}
+
+/**********************************************************************************
+ * FUNCTIONS - getCurrentSumDataPerRow(): These functions perform the conversion of 
+ * dataPerRow and dataPerCol from detail to summary.  This is done so that the  
+ * proper view pane can be calculated on the summary heat map when drawing the 
+ * leftCanvasBox on that side of the screen.
+ **********************************************************************************/
+function getCurrentSumDataPerRow() {
+	var rowSummaryRatio = heatMap.getRowSummaryRatio(MatrixManager.SUMMARY_LEVEL);
+	// Summary data per row for  using the summary ration for that level
+	var	sumDataPerRow = Math.floor(dataPerRow/rowSummaryRatio);
+	return sumDataPerRow;
+}
+// Follow similar methodology for Column as is used in above row based function
+function getCurrentSumDataPerCol() {
+	var colSummaryRatio = heatMap.getColSummaryRatio(MatrixManager.SUMMARY_LEVEL);
+	var	sumDataPerCol = Math.floor(dataPerCol/colSummaryRatio);
+	return sumDataPerCol;
+}
+
+
+/**********************************************************************************
+ * FUNCTIONS - getCurrentDetRow(): These functions perform the conversion of 
+ * currentRow and currentCol coordinates from full matrix position to detail view
+ * position.  This is usually the same but when in ribbon view on a large matrix, 
+ * the positions are scaled.
+ **********************************************************************************/
+function getCurrentDetRow() {
+	var detRow = currentRow;
+	if ((mode == 'RIBBONV') && (selectedStart >= 1)) {
+		var rvRatio = heatMap.getRowSummaryRatio(MatrixManager.RIBBON_VERT_LEVEL);
+		detRow = Math.round(selectedStart/rvRatio);
+	}
+	return  detRow;
+}
+//Follow similar methodology for Column as is used in above row based function
+function getCurrentDetCol() {
+	var detCol = currentCol;
+	if ((mode == 'RIBBONH') && (selectedStart >= 1)) {
+		var rhRatio = heatMap.getColSummaryRatio(MatrixManager.RIBBON_HOR_LEVEL);
+		detCol = Math.round(selectedStart/rhRatio);
+	}
+	return  detCol;
+}
+
+/**********************************************************************************
+ * FUNCTIONS - getCurrentDetDataPerRow(): DataPerRow/Col is in full matrix coordinates
+ * and usually the detail view uses this value directly unless we are in ribbon
+ * view where the value needs to be scaled in one dimension.
+ **********************************************************************************/
+function getCurrentDetDataPerRow() {
+	var	detDataPerRow = dataPerRow;
+	if (mode == 'RIBBONH') {
+		var rate = heatMap.getColSummaryRatio(MatrixManager.RIBBON_HOR_LEVEL);
+		detDataPerRow = Math.round(detDataPerRow/rate);
+	} 
+	return detDataPerRow;
+}
+// Follow similar methodology for Column as is used in above row based function
+function getCurrentDetDataPerCol() {
+	var	detDataPerCol = dataPerCol;
+	if (mode == 'RIBBONV') {
+		var rate = heatMap.getRowSummaryRatio(MatrixManager.RIBBON_VERT_LEVEL);
+		detDataPerCol = Math.round(detDataPerCol/rate);
+	} 
+	return detDataPerCol;
+}
+
+/**********************************************************************************
+ * FUNCTIONS - setDataPerRowFromDet(): DataPerRow/Col is in full matrix coordinates
+ * so sometimes in ribbon view this needs to be translated to full coordinates.
+ **********************************************************************************/
+function setDataPerRowFromDet(detDataPerRow) {
+	dataPerRow = detDataPerRow;
+	if (mode == 'RIBBONH') {
+		if (selectedStart==0) {
+			dataPerRow = heatMap.getNumColumns(MatrixManager.DETAIL_LEVEL);
+		} else {
+			var rate = heatMap.getColSummaryRatio(MatrixManager.RIBBON_HOR_LEVEL);
+			dataPerRow = detDataPerRow * rate;
+		}
+	} 
+}
+// Follow similar methodology for Column as is used in above row based function
+function setDataPerColFromDet(detDataPerCol) {
+	dataPerCol = detDataPerCol;
+	if (mode == 'RIBBONV') {
+		if (selectedStart==0) {
+			dataPerCol = heatMap.getNumRows(MatrixManager.DETAIL_LEVEL);
+		} else {
+			var rate = heatMap.getRowSummaryRatio(MatrixManager.RIBBON_VERT_LEVEL);
+			dataPerCol = detDataPerCol * rate;
+		}
+	} 
+}
+
+
+
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mda_heatmap_viz/static/javascript/SummaryHeatMapDisplay.js	Thu Apr 07 14:43:37 2016 -0400
@@ -0,0 +1,1026 @@
+var BYTE_PER_RGBA = 4;
+
+var canvas;
+var gl; // WebGL contexts
+var textureParams;
+
+//Size of heat map components
+var dendroPaddingHeight = 1;
+var rowDendroHeight = 102; // this is the height of the row dendro in canvas coords (this value may be adjusted eventually by the user)
+var columnDendroHeight = 102; // this is the height of the col dendro in canvas coords (this value may be adjusted eventually by the user)
+var normDendroMatrixHeight = 500; // this is the height of the dendro matrices created in buildDendroMatrix
+var paddingHeight = 2;          // space between classification bars
+var rowClassBarWidth;
+var colClassBarHeight;
+var summaryViewBorderWidth = 2; // black edge around map
+var summaryMatrixWidth;
+var summaryMatrixHeight;
+var colEmptySpace = 0;          // padding for asymmetric maps
+var rowEmptySpace = 0;
+var summaryTotalHeight;
+var summaryTotalWidth;
+
+var rowDendroBars;
+var colDendroBars;
+var colDendroMatrix;
+var rowDendroMatrix;
+var chosenBar = {axis: null, index: null};
+
+var leftCanvasScaleArray = new Float32Array([1.0, 1.0]);
+var leftCanvasBoxLeftTopArray = new Float32Array([0, 0]);
+var leftCanvasBoxRightBottomArray = new Float32Array([0, 0]);
+var leftCanvasTranslateArray = new Float32Array([0, 0]);
+var dendroBoxLeftTopArray = new Float32Array([0, 0]);
+var dendroBoxRightBottomArray = new Float32Array([0, 0]);
+var leftCanvasBoxVertThick;
+var leftCanvasBoxHorThick;
+
+var TexPixels;
+
+var uScale;
+var uTranslate;
+var uBoxLeftTop;
+var uBoxRightBottom;
+var uBoxVertThickness;
+var uBoxHorThickness;
+var uBoxColor;
+var dendroBoxLeftTop;
+var dendroBoxRightBottom;
+var dendroBoxColor;
+var chmInitialized = 0;
+
+var eventTimer = 0; // Used to delay draw updates
+
+//Main function that draws the summary heat map. chmFile is only used in file mode.
+function initSummaryDisplay() {
+	canvas = document.getElementById('summary_canvas');
+	canvas.addEventListener('click',  onClickLeftCanvas);
+	canvas.onmousemove = handleMove;
+	// set the position to (1,1) so that the detail pane loads at the top left corner of the summary.
+	currentRow = 1;
+	currentCol = 1;
+};
+
+// Callback that is notified every time there is an update to the heat map 
+// initialize, new data, etc.  This callback draws the summary heat map.
+function processSummaryMapUpdate (event, level) {   
+
+	if (event == MatrixManager.Event_INITIALIZED) {
+		summaryInit();
+	} else if (event == MatrixManager.Event_NEWDATA && level == MatrixManager.SUMMARY_LEVEL){
+		//Summary tile - wait a bit to see if we get another tile quickly, then draw
+		if (eventTimer != 0) {
+			//New tile arrived - reset timer
+			clearTimeout(eventTimer);
+		}
+		eventTimer = setTimeout(buildSummaryTexture, 200);
+	} 
+	//Ignore updates to other tile types.
+}
+
+// Perform all initialization functions for Summary heat map
+function summaryInit() {
+	var dendroGram = heatMap.getDendrogram();
+	rowDendroHeight = parseInt(dendroGram['row_dendro_height'])+2;
+	columnDendroHeight = parseInt(dendroGram['col_dendro_height'])+2;
+	if (heatMap.showRowDendrogram("SUMMARY")) {
+		rowDendroMatrix = buildDendroMatrix(dendroGram,'Row'); // create array with the bars
+	}
+	if (heatMap.showColDendrogram("SUMMARY")) {
+		colDendroMatrix = buildDendroMatrix(dendroGram,'Column'); // create array with the bars
+	}
+	rowClassBarWidth = calculateTotalClassBarHeight("row");
+	colClassBarHeight = calculateTotalClassBarHeight("column");
+	summaryMatrixWidth = heatMap.getNumColumns(MatrixManager.SUMMARY_LEVEL);
+	summaryMatrixHeight = heatMap.getNumRows(MatrixManager.SUMMARY_LEVEL);
+	
+	//If the matrix is skewed (height vs. width) by more than a 2:1 ratio, add padding to keep the summary from stretching too much.
+	if (summaryMatrixWidth > summaryMatrixHeight && summaryMatrixWidth/summaryMatrixHeight > 2)
+		rowEmptySpace = summaryMatrixWidth/2 - summaryMatrixHeight;
+	else if (summaryMatrixHeight > summaryMatrixWidth && summaryMatrixHeight/summaryMatrixWidth > 2)
+		colEmptySpace = summaryMatrixHeight/2 - summaryMatrixWidth;
+	
+	calcTotalSize();
+
+	canvas.width =  summaryTotalWidth;
+	canvas.height = summaryTotalHeight;
+	setupGl();
+	initGl();
+	buildSummaryTexture();
+	leftCanvasBoxVertThick = .002;//(1+Math.floor(summaryMatrixWidth/250))/1000;
+	leftCanvasBoxHorThick = .002;//(1+Math.floor(summaryMatrixHeight/250))/1000;
+}
+
+//Set the variables for the total size of the summary heat map - used to set canvas, WebGL texture, and viewport size.
+function calcTotalSize() {
+	summaryTotalHeight = summaryMatrixHeight + rowEmptySpace + summaryViewBorderWidth + colClassBarHeight + columnDendroHeight + dendroPaddingHeight;
+	summaryTotalWidth = summaryMatrixWidth + colEmptySpace + summaryViewBorderWidth + rowClassBarWidth + rowDendroHeight + dendroPaddingHeight;
+}
+
+function buildSummaryTexture() {
+	eventTimer = 0;
+
+	var colorMap = heatMap.getColorMapManager().getColorMap("dl1");
+	var colors = colorMap.getColors();
+	var missing = colorMap.getMissingColor();
+	
+	var pos = 0;
+	//If the matrix is skewed, need to pad with space
+	if (rowEmptySpace > 0) 
+		pos = rowEmptySpace * summaryTotalWidth * BYTE_PER_RGBA;
+	
+	//Setup texture to draw on canvas.
+	//Needs to go backward because WebGL draws bottom up.
+	pos += (rowDendroHeight+rowClassBarWidth+ dendroPaddingHeight)*BYTE_PER_RGBA;
+	for (var i = 0; i < heatMap.getNumColumns(MatrixManager.SUMMARY_LEVEL)+summaryViewBorderWidth; i++){
+		TexPixels[pos] = 1; // bottom border
+		TexPixels[pos + 1] = 1;
+		TexPixels[pos + 2] = 1;
+		TexPixels[pos + 3] = 255;
+		pos+=BYTE_PER_RGBA;
+	}
+	pos+=(colEmptySpace*BYTE_PER_RGBA);
+	for (var i = heatMap.getNumRows(MatrixManager.SUMMARY_LEVEL); i > 0; i--) {
+		pos += (rowDendroHeight+rowClassBarWidth+ dendroPaddingHeight)*BYTE_PER_RGBA; // SKIP SPACE RESERVED FOR ROW CLASSBARS + ROW DENDRO
+		TexPixels[pos] = 1; // left border
+		TexPixels[pos + 1] = 1;
+		TexPixels[pos + 2] = 1;
+		TexPixels[pos + 3] = 255;
+		pos+=BYTE_PER_RGBA;
+		for (var j = 1; j <= heatMap.getNumColumns(MatrixManager.SUMMARY_LEVEL); j++) { // draw the heatmap
+			var val = heatMap.getValue(MatrixManager.SUMMARY_LEVEL, i, j);
+			var color = colorMap.getColor(val);
+
+			TexPixels[pos] = color['r'];
+			TexPixels[pos + 1] = color['g'];
+			TexPixels[pos + 2] = color['b'];
+			TexPixels[pos + 3] = color['a'];
+			pos+=BYTE_PER_RGBA;
+		}
+		TexPixels[pos] = 1;	// right border
+		TexPixels[pos + 1] = 1;
+		TexPixels[pos + 2] = 1;
+		TexPixels[pos + 3] = 255;
+		pos+=BYTE_PER_RGBA;	
+		pos+=(colEmptySpace*BYTE_PER_RGBA);
+	}
+	pos += (rowDendroHeight+rowClassBarWidth + dendroPaddingHeight)*BYTE_PER_RGBA;
+	for (var i = 0; i < heatMap.getNumColumns(MatrixManager.SUMMARY_LEVEL)+summaryViewBorderWidth; i++){
+		TexPixels[pos] = 1; // top border
+		TexPixels[pos + 1] = 1;
+		TexPixels[pos + 2] = 1;
+		TexPixels[pos + 3] = 255;
+		pos+=BYTE_PER_RGBA;
+	}
+	
+	// draw column classifications after the map
+	var colClassInfo = getClassBarsToDraw("column");
+	var colClassToDraw = colClassInfo["bars"];
+	var colClassColors = colClassInfo["colors"];
+	drawColClassBars(colClassToDraw,colClassColors,TexPixels);
+	
+	// draw row classifications after that
+	var rowClassInfo = getClassBarsToDraw("row");
+	var rowClassToDraw = rowClassInfo["bars"];
+	var rowClassColors = rowClassInfo["colors"];
+	drawRowClassBars(rowClassToDraw, rowClassColors, TexPixels);
+	
+	
+	// draw the dendrograms at the end of it all
+	if (heatMap.showRowDendrogram("SUMMARY")) {
+		drawRowDendrogram(TexPixels);
+	}
+	if (heatMap.showColDendrogram("SUMMARY")) {
+		drawColumnDendrogram(TexPixels);
+	}
+	drawSummaryHeatMap();
+}
+	
+//WebGL code to draw the summary heat map.
+function drawSummaryHeatMap() {
+	gl.activeTexture(gl.TEXTURE0);
+	gl.texImage2D(
+			gl.TEXTURE_2D, 
+			0, 
+			gl.RGBA, 
+			textureParams['width'], 
+			textureParams['height'], 
+			0, 
+			gl.RGBA,
+			gl.UNSIGNED_BYTE, 
+			TexPixels);
+	gl.uniform2fv(uScale, leftCanvasScaleArray);
+	gl.uniform2fv(uTranslate, leftCanvasTranslateArray);
+	gl.uniform2fv(uBoxLeftTop, leftCanvasBoxLeftTopArray);
+	gl.uniform2fv(uBoxRightBottom, leftCanvasBoxRightBottomArray);
+	gl.uniform1f(uBoxHorThickness, leftCanvasBoxHorThick);
+	gl.uniform1f(uBoxVertThickness, leftCanvasBoxVertThick);
+	gl.uniform4fv(uBoxColor, [1.0, 1.0, 0.0, 1.0]);
+	
+	gl.uniform2fv(dendroBoxLeftTop, dendroBoxLeftTopArray);
+	gl.uniform2fv(dendroBoxRightBottom, dendroBoxRightBottomArray);
+	gl.uniform4fv(dendroBoxColor, [0.0, 1.0, 0.0, 1.0]);
+	gl.drawArrays(gl.TRIANGLE_STRIP, 0, gl.buffer.numItems);
+}
+
+
+//Translate click into row column position and then draw select box.
+function onClickLeftCanvas (evt) {
+	var clickSection = 'Matrix';
+	var xPos = getCanvasX(evt.offsetX);
+	var yPos = getCanvasY(evt.offsetY);
+	var summaryRatio = heatMap.getRowSummaryRatio(MatrixManager.SUMMARY_LEVEL);
+	var sumDataPerRow = Math.floor(dataPerRow/summaryRatio);
+	var sumDataPerCol = Math.floor(dataPerCol/summaryRatio);
+	var sumRow = canvasToMatrixRow(yPos) - Math.floor(sumDataPerCol/2);
+	var sumCol = canvasToMatrixCol(xPos) - Math.floor(sumDataPerRow/2);
+	setCurrentRowFromSum(sumRow);
+	setCurrentColFromSum(sumCol);
+	var col = xPos
+	var colDendroAndClassBars = columnDendroHeight + colClassBarHeight;
+	var row = yPos
+	var rowDendroAndClassBars = rowDendroHeight + rowClassBarWidth;
+	if ((yPos > rowDendroAndClassBars && xPos < columnDendroHeight) && (heatMap.showRowDendrogram("SUMMARY"))) { // row dendro selection
+		clickSection = 'RowDendro';
+		yPos -= colDendroAndClassBars; // yPos = clicked row on canvas
+		
+		var matrixX = (yPos)*pointsPerLeaf*summaryRatio; // matrixX = clicked col of dendro matrix
+		var matrixY = Math.round((rowDendroHeight-xPos)/rowDendroHeight * normDendroMatrixHeight); // matrixY = height of click posiiton on dendro matrix
+		
+		var clickedBar = getTopBar(matrixY,matrixX,'row');
+		var sameBarClicked =true;
+		for (var key in clickedBar){ 
+			if (clickedBar[key] != chosenBar[key]){
+				sameBarClicked = false;
+			}
+		}
+		clearDendroSelection();
+		if (!sameBarClicked){
+			highlightRowDendrogramMatrix(matrixY,matrixX);
+			drawRowDendrogram(TexPixels);
+			chosenBar = clickedBar;
+		}
+	}  else if ((xPos > rowDendroAndClassBars && yPos < columnDendroHeight) && (heatMap.showColDendrogram("SUMMARY"))) { // column dendro selection
+		clickSection = 'ColDendro';
+		xPos-= rowDendroAndClassBars;
+			
+		var matrixX = (xPos)*pointsPerLeaf*summaryRatio; // matrixX = clicked col of dendro matrix
+		var matrixY = Math.round((columnDendroHeight-yPos)/columnDendroHeight * normDendroMatrixHeight) // matrixY = height of click posiiton on dendro matrix
+		
+		var clickedBar = getTopBar(matrixY,matrixX,'column');
+		var sameBarClicked =true;
+		for (var key in clickedBar){ 
+			if (clickedBar[key] != chosenBar[key]){
+				sameBarClicked = false;
+			}
+		}
+		clearDendroSelection();
+		if (!sameBarClicked){
+			highlightColumnDendrogramMatrix(matrixY,matrixX);
+			drawColumnDendrogram(TexPixels);
+			chosenBar = clickedBar;
+		}
+	}
+
+	//Make sure the selected row/column are within the bounds of the matrix.
+	checkRow();
+	checkColumn();
+	
+	if (clickSection=='RowDendro')
+		changeMode('RIBBONV');
+	else if (clickSection == 'ColDendro')
+		changeMode('RIBBONH');
+	else
+		updateSelection();
+}
+
+//Browsers resizes the canvas.  This function translates from a click position
+//back to the original (non-scaled) canvas position. 
+function getCanvasX(offsetX) {
+	return (Math.floor((offsetX/canvas.clientWidth) * canvas.width));
+}
+
+function getCanvasY(offsetY) {
+	return (Math.floor((offsetY/canvas.clientHeight) * canvas.height));
+}
+
+//Return the summary row given an y position on the canvas
+function canvasToMatrixRow(y) {
+	return (y - colClassBarHeight - columnDendroHeight - summaryViewBorderWidth/2);
+} 
+
+function canvasToMatrixCol(x) {
+	return (x - rowClassBarWidth - rowDendroHeight - summaryViewBorderWidth/2);
+}
+
+
+//Given a matrix row, return the canvas position
+function getCanvasYFromRow(row){
+	return (row + colClassBarHeight + columnDendroHeight);
+}
+
+function getCanvasXFromCol(col){
+	return (col + rowClassBarWidth + rowDendroHeight);
+}
+
+/**********************************************************************************
+ * FUNCTION - drawLeftCanvasBox: This function draws the yellow box on the summary
+ * pane whenever the position in the detail pane has changed. (e.g. on load, on click,
+ * on drag, etc...). A conversion is done from detail to summary coordinates, the 
+ * new box position is calculated, and the summary pane is re-drawn.  
+ **********************************************************************************/
+function drawLeftCanvasBox() {
+	var sumRow = getCurrentSumRow();
+	var sumCol = getCurrentSumCol();
+	var	sumDataPerRow = getCurrentSumDataPerRow();
+	var	sumDataPerCol = getCurrentSumDataPerCol();
+	var textureX = getCanvasXFromCol(sumCol) / canvas.width;
+	var textureY = 1.0 - (getCanvasYFromRow(sumRow) / canvas.height);
+	var boxWidth = sumDataPerRow / canvas.width;
+	var boxHeight = sumDataPerCol / canvas.height;
+	leftCanvasBoxLeftTopArray = new Float32Array([textureX, textureY-boxHeight]);
+	leftCanvasBoxRightBottomArray = new Float32Array([textureX + boxWidth, textureY]);
+	
+	drawSummaryHeatMap();
+	
+	//Add selection marks
+	clearSelectionMarks();
+	drawRowSelectionMarks();
+	drawColSelectionMarks();
+}
+
+//WebGL stuff
+
+function setupGl() {
+	gl = canvas.getContext("experimental-webgl", {preserveDrawingBuffer: true});
+	// If standard webgl context cannot be found use experimental-webgl
+	if (!gl) {
+		gl = canvas.getContext('experimental-webgl');
+	}
+	
+	gl.viewportWidth = summaryTotalWidth;
+	gl.viewportHeight = summaryTotalHeight;
+	gl.clearColor(1, 1, 1, 1);
+
+	var program = gl.createProgram();
+	var vertexShader = getVertexShader(gl);
+	var fragmentShader = getFragmentShader(gl);
+	gl.program = program;
+	gl.attachShader(program, vertexShader);
+	gl.attachShader(program, fragmentShader);
+	gl.linkProgram(program);
+	gl.useProgram(program);
+}
+
+
+function getVertexShader(gl) {
+	var source = 'attribute vec2 position;    ' +
+		         'varying vec2 v_texPosition; ' +
+		         'uniform vec2 u_translate;   ' +
+		         'uniform vec2 u_scale;       ' +
+		         'void main () {              ' +
+		         '  vec2 scaledPosition = position * u_scale;               ' +
+		         '  vec2 translatedPosition = scaledPosition + u_translate; ' +
+		         '  gl_Position = vec4(translatedPosition, 0, 1);           ' +
+		         '  v_texPosition = position * 0.5 + 0.5;                   ' +
+		         '}';
+
+
+	var shader = gl.createShader(gl.VERTEX_SHADER);
+	gl.shaderSource(shader, source);
+	gl.compileShader(shader);
+	return shader;
+}
+
+
+function getFragmentShader(gl) {
+	var source = 'precision mediump float;        ' +
+    'varying vec2 v_texPosition;     ' +
+    'varying float v_boxFlag;        ' +
+    'uniform sampler2D u_texture;    ' +
+    'uniform vec2 u_box_left_top;    ' +
+    'uniform vec2 u_box_right_bottom;' +
+    'uniform float u_box_hor_thickness;  ' +
+    'uniform float u_box_vert_thickness;  ' +
+    'uniform vec4 u_box_color;       ' +
+    'uniform vec2 dendro_box_left_top;    ' +
+	'uniform vec2 dendro_box_right_bottom;' +
+	'uniform vec4 dendro_box_color;       ' +
+    'void main () {                  ' +
+    '  vec2 difLeftTop = v_texPosition - u_box_left_top; ' +
+    '  vec2 difRightBottom = v_texPosition - u_box_right_bottom; ' +
+    '  vec2 difDendroLeftTop = v_texPosition - dendro_box_left_top; ' +
+	'  vec2 difDendroRightBottom = v_texPosition - dendro_box_right_bottom; ' +
+	'  if (((v_texPosition.y >= (u_box_left_top.y - u_box_hor_thickness) && v_texPosition.y <= (u_box_right_bottom.y + u_box_hor_thickness)) && ' +
+    '       ((difLeftTop.x <= u_box_vert_thickness && difLeftTop.x >= -u_box_vert_thickness) ||  ' +
+    '        (difRightBottom.x <= u_box_vert_thickness && difRightBottom.x >= -u_box_vert_thickness))) || ' +
+    '       ((v_texPosition.x >= u_box_left_top.x && v_texPosition.x <= u_box_right_bottom.x) && ' +
+    '       ((difLeftTop.y <= u_box_hor_thickness && difLeftTop.y >= -u_box_hor_thickness) || ' +
+    '        (difRightBottom.y <= u_box_hor_thickness && difRightBottom.y >= -u_box_hor_thickness)))) { ' +
+    '   gl_FragColor = u_box_color; ' +
+    '  } else if (((v_texPosition.y >= (dendro_box_left_top.y  - u_box_hor_thickness) && v_texPosition.y <= (dendro_box_right_bottom.y + u_box_hor_thickness)) && ' +
+    '       ((difDendroLeftTop.x <= u_box_vert_thickness && difDendroLeftTop.x >= -u_box_vert_thickness) ||  ' +
+    '        (difDendroRightBottom.x <= u_box_vert_thickness && difDendroRightBottom.x >= -u_box_vert_thickness))) || ' +
+    '       ((v_texPosition.x >= dendro_box_left_top.x && v_texPosition.x <= dendro_box_right_bottom.x) && ' +
+    '       ((difDendroLeftTop.y <= u_box_hor_thickness && difDendroLeftTop.y >= -u_box_hor_thickness) || ' +
+    '        (difDendroRightBottom.y <= u_box_hor_thickness && difDendroRightBottom.y >= -u_box_hor_thickness)))) { ' +
+    '   gl_FragColor = dendro_box_color; ' +
+    '  } else { ' +
+    '   gl_FragColor = texture2D(u_texture, v_texPosition); ' +
+    '  } ' +
+    '}';
+
+
+	var shader = gl.createShader(gl.FRAGMENT_SHADER);;
+	gl.shaderSource(shader, source);
+	gl.compileShader(shader);
+	return shader;
+}
+
+
+
+function initGl () {
+	gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);
+	gl.clear(gl.COLOR_BUFFER_BIT);
+
+	// Vertices
+	var buffer = gl.createBuffer();
+	gl.buffer = buffer;
+	gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
+	var vertices = [ -1, -1, 1, -1, 1, 1, -1, -1, -1, 1, 1, 1 ];
+	gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
+	var byte_per_vertex = Float32Array.BYTES_PER_ELEMENT;
+	var component_per_vertex = 2;
+	buffer.numItems = vertices.length / component_per_vertex;
+	var stride = component_per_vertex * byte_per_vertex;
+	var program = gl.program;
+	var position = gl.getAttribLocation(program, 'position');
+	uScale = gl.getUniformLocation(program, 'u_scale');
+	uTranslate = gl.getUniformLocation(program, 'u_translate');
+	uBoxLeftTop = gl.getUniformLocation(program, 'u_box_left_top');
+	uBoxRightBottom = gl.getUniformLocation(program, 'u_box_right_bottom');
+	uBoxHorThickness = gl.getUniformLocation(program, 'u_box_hor_thickness');
+	uBoxVertThickness = gl.getUniformLocation(program, 'u_box_vert_thickness');
+	uBoxColor = gl.getUniformLocation(program, 'u_box_color');
+	
+	dendroBoxLeftTop = gl.getUniformLocation(program, 'dendro_box_left_top');
+	dendroBoxRightBottom = gl.getUniformLocation(program, 'dendro_box_right_bottom');
+	dendroBoxColor = gl.getUniformLocation(program, 'dendro_box_color');
+	
+	gl.enableVertexAttribArray(position);
+	gl.vertexAttribPointer(position, 2, gl.FLOAT, false, stride, 0);
+
+	// Texture
+	var texture = gl.createTexture();
+	gl.bindTexture(gl.TEXTURE_2D, texture);
+	gl.texParameteri(
+			gl.TEXTURE_2D, 
+			gl.TEXTURE_WRAP_S, 
+			gl.CLAMP_TO_EDGE);
+	gl.texParameteri(
+			gl.TEXTURE_2D, 
+			gl.TEXTURE_WRAP_T, 
+			gl.CLAMP_TO_EDGE);
+	gl.texParameteri(
+			gl.TEXTURE_2D, 
+			gl.TEXTURE_MIN_FILTER,
+			gl.NEAREST);
+	gl.texParameteri(
+			gl.TEXTURE_2D, 
+			gl.TEXTURE_MAG_FILTER, 
+			gl.NEAREST);
+	
+	textureParams = {};
+	var texWidth = null, texHeight = null, texData;
+	texWidth = summaryTotalWidth;
+	texHeight = summaryTotalHeight;
+	texData = new ArrayBuffer(texWidth * texHeight * BYTE_PER_RGBA);
+	TexPixels = new Uint8Array(texData);
+	textureParams['width'] = texWidth;
+	textureParams['height'] = texHeight;
+}
+
+//=====================//
+// 	CLASSBAR FUNCTIONS //
+//=====================//
+
+// returns all the classifications bars for a given axis and their corresponding color schemes in an array.
+function getClassBarsToDraw(axis){
+	var classBars = heatMap.getClassifications();
+	var barsAndColors = {"bars":[], "colors":[]};
+	for (var key in classBars){
+		if (classBars[key].position == axis){
+			barsAndColors["bars"].push(key);
+			barsAndColors["colors"].push(classBars[key].colorScheme);
+		}
+	}
+	return barsAndColors;
+}
+
+// draws row classification bars into the texture array ("dataBuffer"). "names"/"colorSchemes" should be array of strings.
+function drawColClassBars(names,colorSchemes,dataBuffer){
+	var classBars = heatMap.getClassifications(); 
+	var colorMapMgr = heatMap.getColorMapManager();
+	var pos = (summaryTotalWidth)*(rowEmptySpace+summaryMatrixHeight+summaryViewBorderWidth)*BYTE_PER_RGBA;
+	for (var i = 0; i < names.length; i++){	//for each column class bar we draw...
+		var currentClassBar = classBars[names[i]];
+		if (currentClassBar.show === 'Y') {
+			var colorMap = colorMapMgr.getColorMap(colorSchemes[i]); // assign the proper color scheme...
+			var classBarValues = currentClassBar.values;
+			var classBarLength = currentClassBar.values.length;
+			if (typeof currentClassBar.svalues != 'undefined') {
+				classBarValues = currentClassBar.svalues;
+				classBarLength = currentClassBar.svalues.length;
+			}
+			pos += (summaryTotalWidth)*paddingHeight*BYTE_PER_RGBA; // draw padding between class bars
+			var line = new Uint8Array(new ArrayBuffer(classBarLength * BYTE_PER_RGBA)); // save a copy of the class bar
+			var loc = 0;
+			for (var k = 0; k < classBarLength; k++) { 
+				var val = classBarValues[k];
+				var color = colorMap.getClassificationColor(val);
+				if (val == "null") {
+					color = colorMap.getHexToRgba(colorMap.getMissingColor());
+				}
+				line[loc] = color['r'];
+				line[loc + 1] = color['g'];
+				line[loc + 2] = color['b'];
+				line[loc + 3] = color['a'];
+				loc += BYTE_PER_RGBA;
+			}
+			loc = 0;
+			for (var j = 0; j < currentClassBar.height-paddingHeight; j++){ // draw the class bar into the dataBuffer
+				pos += (rowDendroHeight+dendroPaddingHeight+rowClassBarWidth+summaryViewBorderWidth/2)*BYTE_PER_RGBA;
+				for (var k = 0; k < line.length; k++) { 
+					dataBuffer[pos] = line[k];
+					pos++;
+				}
+				pos += (summaryViewBorderWidth/2)*BYTE_PER_RGBA;
+				pos += (colEmptySpace*BYTE_PER_RGBA);
+			}
+		}
+	}
+}
+
+// draws row classification bars into the texture array ("dataBuffer"). "names"/"colorSchemes" should be array of strings.
+function drawRowClassBars(names,colorSchemes,dataBuffer){
+	var offset = ((rowEmptySpace*summaryTotalWidth)+(summaryTotalWidth+rowDendroHeight))*BYTE_PER_RGBA;
+	var colorMapMgr = heatMap.getColorMapManager();
+	var classBars = heatMap.getClassifications();
+	for (var i = 0; i < names.length; i++){
+		var pos = 0 + offset;
+		var colorMap = colorMapMgr.getColorMap(colorSchemes[i]);
+		var currentClassBar = classBars[names[i]];
+		if (currentClassBar.show === 'Y') {
+			var classBarValues = currentClassBar.values;
+			var classBarLength = currentClassBar.values.length;
+			if (typeof currentClassBar.svalues != 'undefined') {
+				classBarValues = currentClassBar.svalues;
+				classBarLength = currentClassBar.svalues.length;
+			}
+			for (var j = classBarLength; j > 0; j--){
+				var val = classBarValues[j-1];
+				var color = colorMap.getClassificationColor(val);
+				if (val == "null") {
+					color = colorMap.getHexToRgba(colorMap.getMissingColor());
+				}
+				for (var k = 0; k < currentClassBar.height-paddingHeight; k++){
+					dataBuffer[pos] = color['r'];
+					dataBuffer[pos + 1] = color['g'];
+					dataBuffer[pos + 2] = color['b'];
+					dataBuffer[pos + 3] = color['a'];
+					pos+=BYTE_PER_RGBA;	// 4 bytes per color
+				}
+				// padding between class bars
+				pos+=paddingHeight*BYTE_PER_RGBA;
+				pos+=(summaryTotalWidth - rowClassBarWidth)*BYTE_PER_RGBA;
+			}
+			offset+= currentClassBar.height;
+		}
+	}
+}
+
+
+// increase the height/width of a classbar and resize the map texture as well. redraws when done.
+function increaseClassBarHeight(name){
+	var classBars = heatMap.getClassifications();
+	if (classBars[name].height < paddingHeight){
+		classBars[name].height = paddingHeight +1; // if class bar isn't visible, then make it 1 px taller than the padding height
+	} else {
+		classBars[name].height += 2;
+	}
+	classBarHeight = calculateTotalClassBarHeight("column");
+	classBarWidth = calculateTotalClassBarHeight("row");
+	calcTotalSize();
+	var texWidth = null, texHeight = null, texData;
+	texWidth = summaryTotalWidth;
+	texHeight = summaryTotalHeight;
+	texData = new ArrayBuffer(texWidth * texHeight * BYTE_PER_RGBA);
+	TexPixels = new Uint8Array(texData);
+	textureParams['width'] = texWidth;
+	textureParams['height'] = texHeight;
+	drawSummaryHeatMap();
+}
+
+// decrease the height/width of a classbar and resize the map texture as well. redraws when done.
+function decreaseClassBarHeight(name){
+	var classBars = heatMap.getClassifications();
+	classBars[name].height -= 2;
+	if (classBars[name].height < paddingHeight){
+		classBars[name].height = 0; // if the class bar is going to be shorter than the padding height, make it invisible
+	}
+	classBarHeight = calculateTotalClassBarHeight("column");
+	classBarWidth = calculateTotalClassBarHeight("row");
+	calcTotalSize();
+	var texWidth = null, texHeight = null, texData;
+	texWidth = summaryTotalWidth;
+	texHeight = summaryTotalHeight;
+	texData = new ArrayBuffer(texWidth * texHeight * BYTE_PER_RGBA);
+	TexPixels = new Uint8Array(texData);
+	textureParams['width'] = texWidth;
+	textureParams['height'] = texHeight;
+	drawSummaryHeatMap();
+}
+
+
+function calculateTotalClassBarHeight(axis){
+	var totalHeight = 0;
+	var classBars = heatMap.getClassifications();
+	for (var key in classBars){
+		if (classBars[key].position == axis){
+			if (classBars[key].show === 'Y') {
+				totalHeight += classBars[key].height;
+			}
+		}
+	}
+	return totalHeight;
+}
+
+
+//=======================//
+//	DENDROGRAM FUNCTIONS //
+//=======================//
+
+//
+var pointsPerLeaf = 3; // each leaf will get 3 points in the dendrogram array. This is to avoid lines being right next to each other
+
+function colDendroMatrixCoordToTexturePos(matrixRow,matrixCol){ // convert the matrix coord to the data buffer position (start of the RGBA block)
+	var summaryRatio = heatMap.getRowSummaryRatio(MatrixManager.SUMMARY_LEVEL);
+	var mapx = Math.round(matrixCol/pointsPerLeaf/summaryRatio);
+	var mapy = Math.round(matrixRow/normDendroMatrixHeight * columnDendroHeight);
+	var pos = (summaryTotalWidth) *(mapy+rowEmptySpace+summaryViewBorderWidth+summaryMatrixHeight+colClassBarHeight)*BYTE_PER_RGBA; // go to proper height
+	pos += (rowDendroHeight + dendroPaddingHeight+ rowClassBarWidth + summaryViewBorderWidth/2 + mapx)*BYTE_PER_RGBA;
+	return pos;
+}
+
+function rowDendroMatrixCoordToTexturePos(matrixRow,matrixCol){ // convert matrix coord to data buffer position (leftmost column of matrix corresponds to the top row of the map)
+	var summaryRatio = heatMap.getRowSummaryRatio(MatrixManager.SUMMARY_LEVEL);
+	var mapx = rowDendroHeight - Math.round(matrixRow/normDendroMatrixHeight * rowDendroHeight); // bottom most row of matrix is at the far-right of the map dendrogram
+	var mapy = summaryMatrixHeight - Math.round(matrixCol/pointsPerLeaf/summaryRatio); // matrix column 1 is the top row of the map
+	var pos = (summaryTotalWidth)*(mapy+rowEmptySpace)*BYTE_PER_RGBA; // pass the empty space (if any) and the border width, to get to the height on the map
+	pos += mapx*BYTE_PER_RGBA;
+	return pos;
+}
+
+
+function drawColumnDendrogram(dataBuffer){
+	var mod,firstSkip,skipInterval;
+	if (columnDendroHeight > normDendroMatrixHeight){
+		mod = rowDendroHeight % normDendroMatrixHeight; // a row may have to be drawn twice in case there is a rounding error from matrix to texture
+		firstSkip = Math.round(normDendroMatrixHeight/mod/2);
+		skipInterval = Math.round(normDendroMatrixHeight/mod);
+	}
+	
+	for (var i = 0; i <= colDendroMatrix.length+1; i++){
+		var line = colDendroMatrix[i]; // line = each row of the col dendro matrix
+		for (var j  in line){
+			var pos = colDendroMatrixCoordToTexturePos(i,j);
+			if (colDendroMatrix[i][j] == 1){
+				dataBuffer[pos] = 3,dataBuffer[pos+1] = 3,dataBuffer[pos+2] = 3,dataBuffer[pos+3] = 255;
+			} else if (colDendroMatrix[i][j] == 2){
+				dataBuffer[pos] = 3,dataBuffer[pos+1] = 255,dataBuffer[pos+2] = 3,dataBuffer[pos+3] = 255;
+			}
+		}
+		if (i !=0 && (i % skipInterval == 0 || i % skipInterval == skipInterval/2)){ // if there was a rounding error made, redraw the dendro line on previous line
+			for (var j  in line){
+				var pos = colDendroMatrixCoordToTexturePos(i,j) - summaryTotalWidth*BYTE_PER_RGBA;
+				if (colDendroMatrix[i][j] == 1){
+					dataBuffer[pos] = 3,dataBuffer[pos+1] = 3,dataBuffer[pos+2] = 3,dataBuffer[pos+3] = 255;
+				} else if (colDendroMatrix[i][j] == 2){
+					dataBuffer[pos] = 3,dataBuffer[pos+1] = 255,dataBuffer[pos+2] = 3,dataBuffer[pos+3] = 255;
+				}
+			}
+		}
+	}
+}
+
+
+function drawRowDendrogram(dataBuffer){
+	var mod,firstSkip,skipInterval;
+	if (rowDendroHeight > normDendroMatrixHeight){
+		mod = rowDendroHeight % normDendroMatrixHeight; // a row may have to be drawn twice in case there is a rounding error from matrix to texture
+		firstSkip = Math.round(normDendroMatrixHeight/mod/2);
+		skipInterval = Math.round(normDendroMatrixHeight/mod);
+	}
+	
+	for (var i = 0; i <= rowDendroMatrix.length+1; i++){
+		var line = rowDendroMatrix[i]; // line = each row of the col dendro matrix
+		for (var j  in line){
+			var pos = rowDendroMatrixCoordToTexturePos(i,j);
+			if (rowDendroMatrix[i][j] == 1){
+				dataBuffer[pos] = 3,dataBuffer[pos+1] = 3,dataBuffer[pos+2] = 3,dataBuffer[pos+3] = 255;
+			} else if (rowDendroMatrix[i][j] == 2){
+				dataBuffer[pos] = 3,dataBuffer[pos+1] = 255,dataBuffer[pos+2] = 3,dataBuffer[pos+3] = 255;
+			}
+		}
+		if (i !=0 && (i % skipInterval == 0 || i % skipInterval == skipInterval/2)){ // if there was a rounding error made, redraw the dendro line on previous line
+			for (var j  in line){
+				var pos = rowDendroMatrixCoordToTexturePos(i,j) + BYTE_PER_RGBA;
+				if (rowDendroMatrix[i][j] == 1){
+					dataBuffer[pos] = 3,dataBuffer[pos+1] = 3,dataBuffer[pos+2] = 3,dataBuffer[pos+3] = 255;
+				} else if (rowDendroMatrix[i][j] == 2){
+					dataBuffer[pos] = 3,dataBuffer[pos+1] = 255,dataBuffer[pos+2] = 3,dataBuffer[pos+3] = 255;
+				}
+			}
+		}
+	}
+}
+
+function getTranslatedLocation(location){
+	var summaryRatio = heatMap.getRowSummaryRatio(MatrixManager.SUMMARY_LEVEL);
+	return Math.round((location/summaryRatio)/pointsPerLeaf);
+}
+
+//creates an array of bar objects from the dendrogram info
+function buildDendroMatrix(dendroData,axis){
+	var numNodes = dendroData[axis].length;
+	var bars = [];
+	var lastRow = dendroData[axis][numNodes-1];
+	var maxHeight = Number(lastRow.split(",")[2]); // this assumes the heightData is ordered from lowest height to highest
+	var matrix = new Array(normDendroMatrixHeight+1);
+	for (var i = 0; i < normDendroMatrixHeight+1; i++){ // 500rows * (3xWidth)cols matrix
+		matrix[i] = new Array(pointsPerLeaf*heatMap.getNumColumns('d'));
+	}
+	for (var i = 0; i < numNodes; i++){
+		var tokes = dendroData[axis][i].split(",");
+		var leftIndex = Number(tokes[0]); // index is the location of the bar in the clustered data
+		var rightIndex = Number(tokes[1]);
+		var height = Number(tokes[2]);
+		var leftLoc = findLocationFromIndex(leftIndex); // this is the position it occupies in the dendroMatrix space
+		var rightLoc = findLocationFromIndex(rightIndex);
+		var normHeight = Math.round(normDendroMatrixHeight*height/maxHeight);
+		bars.push({"left":leftLoc, "right":rightLoc, "height":normHeight});
+		for (var j = leftLoc; j < rightLoc; j++){
+			matrix[normHeight][j] = 1;
+		}
+		var drawHeight = normHeight-1;
+		while (drawHeight > 0 && matrix[drawHeight][leftLoc] != 1){			
+			matrix[drawHeight][leftLoc] = 1;
+			drawHeight--;
+		}
+		drawHeight = normHeight;
+		while (matrix[drawHeight][rightLoc] != 1 && drawHeight > 0){			
+			matrix[drawHeight][rightLoc] = 1;
+			drawHeight--;
+		}
+	}
+	
+	if (axis == 'Column'){
+		colDendroBars = bars;
+	} else {
+		rowDendroBars = bars;
+	}
+	return matrix;
+	
+	// returns the position in terms of the 3N space
+	function findLocationFromIndex(index){
+		if (index < 0){
+			index = 0-index; // make index a positive number to find the leaf
+			return pointsPerLeaf*index-2; // all leafs should occupy the middle space of the 3 points available
+		} else {
+			index--;
+			return Math.round((bars[index].left + bars[index].right)/2); // gets the middle point of the bar
+		}
+	}
+}
+
+function getTopBar(i,j,axis){
+	var dendroMatrix;
+	if (axis == "row")dendroMatrix = rowDendroMatrix;
+	else dendroMatrix = colDendroMatrix;
+	while (dendroMatrix[i][j]==undefined){ i--;}// find the first line that is below the clicked coord
+	var leftAndRightExtremes = exploreToEndOfBar(i,j,dendroMatrix); // find the endpoints of the highest level node
+	return {axis: axis, leftEnd: leftAndRightExtremes[0], rightEnd: leftAndRightExtremes[1], height: i}
+}
+
+
+
+function highlightRowDendrogramMatrix(i, j){ // i-th row, j-th column of dendro matrix
+	var leftExtreme, rightExtreme;
+	while (rowDendroMatrix[i][j]==undefined){i--;} // find the first line that is below the clicked coord
+	var leftAndRightExtremes = exploreToEndOfBar(i,j,rowDendroMatrix); // find the endpoints of the highest level node
+	leftExtreme = leftAndRightExtremes[0], rightExtreme = leftAndRightExtremes[1];
+	leftExtreme = findLeftEnd(i,leftExtreme,rowDendroMatrix);
+	rightExtreme = findRightEnd(i,rightExtreme,rowDendroMatrix); // L and R extreme values are in dendro matrix coords right now
+	highlightAllBranchesInRange(i,leftExtreme,rightExtreme,rowDendroMatrix);
+	
+	leftExtreme = getTranslatedLocation(leftExtreme); // L and R extreme values gets converted to heatmap locations
+	rightExtreme = getTranslatedLocation(rightExtreme);
+
+	// Draw green dendrogram box over summary heatmap
+	var matrixBottom = rowEmptySpace / canvas.height + leftCanvasBoxHorThick;
+	var matrixRight = colEmptySpace / canvas.width + leftCanvasBoxVertThick;
+	var leftMin = leftCanvasBoxVertThick + ((rowClassBarWidth+rowDendroHeight)/canvas.width);
+	var topMin = leftCanvasBoxHorThick + ((colClassBarHeight+columnDendroHeight)/canvas.height);
+	dendroBoxLeftTopArray = new Float32Array([leftMin, 1-rightExtreme/canvas.height-topMin]);
+	dendroBoxRightBottomArray = new Float32Array([1-matrixRight, 1-leftExtreme/canvas.height-topMin]);
+	// Set start and stop coordinates
+	var rvRatio = heatMap.getRowSummaryRatio(MatrixManager.RIBBON_VERT_LEVEL);
+	var summaryRatio = heatMap.getRowSummaryRatio(MatrixManager.SUMMARY_LEVEL);
+	selectedStart = Math.round(leftExtreme*summaryRatio) +1;
+	selectedStop = Math.round(rightExtreme*summaryRatio) +1;
+}
+
+function highlightColumnDendrogramMatrix(i,j){
+	var leftExtreme, rightExtreme;
+	while (colDendroMatrix[i][j]==undefined){ i--;}
+	var leftAndRightExtremes = exploreToEndOfBar(i,j,colDendroMatrix); // find the endpoints of the highest level node
+	leftExtreme = leftAndRightExtremes[0], rightExtreme = leftAndRightExtremes[1];
+	
+	leftExtreme = findLeftEnd(i,leftExtreme,colDendroMatrix);
+	rightExtreme = findRightEnd(i,rightExtreme,colDendroMatrix); // L and R extreme values are in dendro matrix coords right now
+	highlightAllBranchesInRange(i,leftExtreme,rightExtreme,colDendroMatrix);
+	
+	leftExtreme = getTranslatedLocation(leftExtreme); // L and R extreme values gets converted to heatmap locations
+	rightExtreme = getTranslatedLocation(rightExtreme);
+	
+	// Draw green dendrogram box over summary heatmap
+	var matrixBottom = rowEmptySpace / canvas.height + leftCanvasBoxHorThick;
+	var matrixRight = colEmptySpace / canvas.width + leftCanvasBoxVertThick;
+	var leftMin = leftCanvasBoxVertThick + ((rowClassBarWidth+rowDendroHeight)/canvas.width);
+	var topMin = leftCanvasBoxHorThick + ((colClassBarHeight+columnDendroHeight)/canvas.height);
+	dendroBoxLeftTopArray = new Float32Array([leftExtreme/canvas.width+leftMin, matrixBottom]); 
+	dendroBoxRightBottomArray = new Float32Array([rightExtreme/canvas.width+leftMin, 1-topMin]);  
+	// Set start and stop coordinates
+	var rhRatio = heatMap.getColSummaryRatio(MatrixManager.RIBBON_HOR_LEVEL);
+	var summaryRatio = heatMap.getRowSummaryRatio(MatrixManager.SUMMARY_LEVEL);
+	selectedStart = Math.round(leftExtreme*summaryRatio) +1;
+	selectedStop = Math.round(rightExtreme*summaryRatio) +1;
+}
+
+function exploreToEndOfBar(i,j, dendroMatrix){
+	var leftExtreme = j, rightExtreme = j;
+	dendroMatrix[i][j] = 2;
+	while (dendroMatrix[i][rightExtreme+1]==1 || dendroMatrix[i][rightExtreme+1]==2){ // now find the right and left end points of the line in the matrix and highlight as we go
+		rightExtreme++;
+		dendroMatrix[i][rightExtreme] = 2;
+	}
+	while (dendroMatrix[i][leftExtreme-1]==1 || dendroMatrix[i][leftExtreme-1]==2){
+		leftExtreme--;
+		dendroMatrix[i][leftExtreme] = 2;
+	}
+	return [leftExtreme,rightExtreme];
+}
+
+
+function findLeftEnd(i,j,dendroMatrix){
+	dendroMatrix[i][j] = 2;
+	while (i != 0 && j != 0){ // as long as we aren't at the far left or the very bottom, keep moving
+		if (dendroMatrix[i][j-1] == 1 ||dendroMatrix[i][j-1] == 2){ // can we keep moving left?
+			j--;
+			dendroMatrix[i][j] = 2;
+		} else {//if (dendroMatrix[i-1][j] == 1 ||dendroMatrix[i-1][j] == 2){ // can we move towards the bottom?
+			i--;
+			dendroMatrix[i][j] = 2;
+		}
+	}
+	return j;
+}
+
+function findRightEnd(i,j,dendroMatrix){
+	dendroMatrix[i][j] = 2;
+	while (i != 0 && j <= dendroMatrix[1].length){
+		if (dendroMatrix[i][j+1] == 1 ||dendroMatrix[i][j+1] == 2){
+			j++;
+			dendroMatrix[i][j] = 2;
+		} else {//if (dendroMatrix[i-1][j] == 1 ||dendroMatrix[i-1][j] == 2){
+			i--;
+			dendroMatrix[i][j] = 2;
+		}
+	}
+	return j;
+}
+
+function highlightAllBranchesInRange(height,leftExtreme,rightExtreme,dendroMatrix){
+	for (var i = height; i >= 0; i--){
+		for (var loc in dendroMatrix[i]){
+			if (leftExtreme < loc && loc < rightExtreme){
+				dendroMatrix[i][loc] = 2;
+			}
+		}
+	}
+}
+
+function clearDendroSelection(){
+	chosenBar = {axis: null, index: null};
+	selectedStart = 0;
+	selectedStop = 0;
+	if (!isSub) {
+		dendroBoxLeftTopArray = new Float32Array([0, 0]);
+		dendroBoxRightBottomArray = new Float32Array([0, 0]);
+		if (heatMap.showColDendrogram("summary")) {
+			colDendroMatrix = buildDendroMatrix(heatMap.getDendrogram(),'Column');
+			drawColumnDendrogram(TexPixels);
+		}
+		if (heatMap.showRowDendrogram("summary")) {
+			rowDendroMatrix = buildDendroMatrix(heatMap.getDendrogram(),"Row");
+			drawRowDendrogram(TexPixels);
+		}
+		drawSummaryHeatMap();
+	}
+}
+
+
+//***************************//
+//Selection Label Functions *//
+//***************************//
+function summaryResize() {
+	clearSelectionMarks();
+	drawRowSelectionMarks();
+	drawColSelectionMarks();
+}
+
+
+function drawRowSelectionMarks() {
+	var markElement = document.getElementById('sumlabelDiv');
+	var headerSize = summaryTotalHeight - summaryMatrixHeight;
+
+	var fontSize = 10;
+	var selectedRows = getSearchRows();
+	
+	
+	for (var i = 0; i < selectedRows.length; i++) {
+		var xPos = (1-colEmptySpace/canvas.width)*canvas.clientWidth + 3;
+		var position = headerSize + (selectedRows[i]/heatMap.getRowSummaryRatio(MatrixManager.SUMMARY_LEVEL));
+		var yPos = ((position /summaryTotalHeight-(rowEmptySpace/canvas.height))* canvas.clientHeight) - fontSize;
+		addLabelDiv(markElement, 'sum_row' + i, 'MarkLabel', '<', xPos, yPos, fontSize, 'F');
+	}
+}
+
+function drawColSelectionMarks() {
+	var markElement = document.getElementById('sumlabelDiv');
+	var headerSize = summaryTotalWidth - summaryMatrixWidth;
+
+	var fontSize = 10;
+	var selectedCols = getSearchCols();
+	
+	
+	for (var i = 0; i < selectedCols.length; i++) {
+		var position = headerSize + (selectedCols[i]/heatMap.getColSummaryRatio(MatrixManager.SUMMARY_LEVEL));
+		var xPos = ((position / summaryTotalWidth-(colEmptySpace/canvas.width))*(canvas.clientWidth)) + fontSize/2;
+		var yPos = (1-rowEmptySpace/canvas.height)*canvas.clientHeight + 4;
+		addLabelDiv(markElement, 'sum_row' + i, 'MarkLabel', '<', xPos, yPos, fontSize, 'T');
+	}
+}
+
+function clearSelectionMarks() {
+	var markElement = document.getElementById('sumlabelDiv');
+	var oldMarks = document.getElementsByClassName("MarkLabel");
+	while (oldMarks.length > 0) {
+		markElement.removeChild(oldMarks[0]);
+	}
+
+}
+
+
+function dividerStart(){
+	userHelpClose();
+	document.addEventListener('mousemove', dividerMove);
+	document.addEventListener('touchmove', dividerMove);
+	document.addEventListener('mouseup', dividerEnd);
+	document.addEventListener('touchend',dividerEnd);
+}
+function dividerMove(e){
+	var divider = document.getElementById('divider');
+	if (e.touches){
+    	if (e.touches.length > 1){
+    		return false;
+    	}
+    }
+	var Xmove = e.touches ? divider.offsetLeft - e.touches[0].pageX : divider.offsetLeft - e.pageX;
+	var summary = document.getElementById('summary_chm');
+	var summaryX = summary.offsetWidth - Xmove;
+	summary.setAttribute("style","position: relative; width:" + summaryX + "px");
+	var detail = document.getElementById('detail_chm');
+	var detailX = detail.offsetWidth + Xmove;
+	detail.setAttribute("style","position: relative; width:" + detailX + "px");
+	clearLabels();
+	clearSelectionMarks();
+}
+function dividerEnd(){
+	document.removeEventListener('mousemove', dividerMove);
+	document.removeEventListener('mouseup', dividerEnd);
+	document.removeEventListener('touchmove',dividerMove);
+	document.removeEventListener('touchend',dividerEnd);
+	detailResize();
+	summaryResize();
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mda_heatmap_viz/static/javascript/UserHelpManager.js	Thu Apr 07 14:43:37 2016 -0400
@@ -0,0 +1,527 @@
+/**********************************************************************************
+ * USER HELP FUNCTIONS:  The following functions handle the processing 
+ * for user help popup windows for the detail canvas and the detail canvas buttons.
+ **********************************************************************************/
+
+/**********************************************************************************
+ * FUNCTION - userHelpOpen: This function handles all of the tasks necessary to 
+ * generate help pop-up panels for the detail heat map and the detail heat map 
+ * classification bars.  
+ **********************************************************************************/
+function userHelpOpen(e){ 
+    userHelpClose();
+    clearTimeout(detailPoint);
+    var orgW = window.innerWidth+window.pageXOffset;
+    var orgH = window.innerHeight+window.pageYOffset;
+    var helptext = getDivElement("helptext");    
+    helptext.style.position = "absolute";
+    document.getElementsByTagName('body')[0].appendChild(helptext);
+    var rowElementSize = dataBoxWidth * detCanvas.clientWidth/detCanvas.width; // px/Glpoint
+    var colElementSize = dataBoxHeight * detCanvas.clientHeight/detCanvas.height;
+
+    // pixels
+    var rowClassWidthPx = getRowClassPixelWidth();
+    var colClassHeightPx = getColClassPixelHeight();
+    var rowDendroWidthPx =  getRowDendroPixelWidth();
+    var colDendroHeightPx = getColDendroPixelHeight();
+	var mapLocY = e.layerY - colClassHeightPx - colDendroHeightPx;
+	var mapLocX = e.layerX - rowClassWidthPx - rowDendroWidthPx;
+	
+    if (isOnObject(e,"map")) {
+    	var row = Math.floor(currentRow + (mapLocY/colElementSize)*getSamplingRatio('row'));
+    	var col = Math.floor(currentCol + (mapLocX/rowElementSize)*getSamplingRatio('col'));
+    	var rowLabels = heatMap.getRowLabels().labels;
+    	var colLabels = heatMap.getColLabels().labels;
+    	var classBars = heatMap.getClassifications();
+    	var helpContents = document.createElement("TABLE");
+    	setTableRow(helpContents, ["<u>"+"Data Details"+"</u>", "&nbsp;"], 2);
+    	setTableRow(helpContents,["&nbsp;Value:", heatMap.getValue(MatrixManager.DETAIL_LEVEL,row,col).toFixed(5)]);
+    	setTableRow(helpContents,[ "&nbsp;Row:", rowLabels[row-1]]);
+    	setTableRow(helpContents,["&nbsp;Column:", colLabels[col-1]]);
+    	helpContents.insertRow().innerHTML = formatBlankRow();
+    	var rowCtr = 8;
+    	var writeFirstCol = true;
+    	var pos = col;
+    	var classLen = Object.keys(classBars).length;
+    	if (classLen > 0) {
+			setTableRow(helpContents, ["&nbsp;<u>"+"Row Classifications"+"</u>", "&nbsp;"], 2);
+	    	for (var key in classBars){
+	    		if (classBars[key].position == "column") {
+	    			if (writeFirstCol) {
+	    		    	helpContents.insertRow().innerHTML = formatBlankRow();
+	    				setTableRow(helpContents, ["&nbsp;<u>"+"Column Classifications"+"</u>", "&nbsp;"], 2);
+	    				writeFirstCol = false;
+	    			}
+	        		pos = row;
+	        		rowCtr = rowCtr++;
+	    		}
+	    		setTableRow(helpContents,["&nbsp;&nbsp;&nbsp;"+key+":"+"</u>", classBars[key].values[pos-1]]);	    		
+	    		rowCtr++;
+	    	}
+    	}
+        helptext.style.display="inherit";
+    	helptext.appendChild(helpContents);
+    	locateHelpBox(e, helptext);
+    } else if (isOnObject(e,"rowClass") || isOnObject(e,"colClass")) {
+    	var pos, classInfo, names, colorSchemes, value;
+    	var classBars = heatMap.getClassifications();
+    	var hoveredBar, hoveredBarColorScheme;                                                     //coveredWidth = 0, coveredHeight = 0;
+    	if (isOnObject(e,"colClass")) {
+    		var coveredHeight = detCanvas.clientHeight*detailDendroHeight/detCanvas.height
+    		pos = Math.floor(currentCol + (mapLocX/rowElementSize));
+    		classInfo = getClassBarsToDraw("column");
+        	names = classInfo["bars"];
+        	colorSchemes = classInfo["colors"];
+        	for (var i = names.length-1; i >= 0; i--) { // find which class bar the mouse is over
+        		var currentBar = names[i];
+    			var bar =  classBars[currentBar];
+    			if ((bar.show === 'Y') && (bar.position === 'column')) {
+	        		coveredHeight += detCanvas.clientHeight*classBars[currentBar].height/detCanvas.height;
+	        		if (coveredHeight >= e.layerY) {
+	        			hoveredBar = currentBar;
+	        			hoveredBarColorScheme = colorSchemes[i];
+	        			break;
+	        		}
+    			}
+        	} 
+    	} else {
+    		var coveredWidth = detCanvas.clientHeight*detailDendroWidth/detCanvas.height
+    		pos = Math.floor(currentRow + (mapLocY/colElementSize));
+    		classInfo = getClassBarsToDraw("row");
+        	names = classInfo["bars"];
+        	colorSchemes = classInfo["colors"];
+        	for (var i = names.length-1; i >= 0; i--){ // find which class bar the mouse is over
+        		var currentBar = names[i];
+    			var bar =  classBars[currentBar];
+    			if ((bar.show === 'Y') && (bar.position === 'row')) {
+	        		coveredWidth += detCanvas.clientWidth*classBars[currentBar].height/detCanvas.width;
+	        		if (coveredWidth >= e.layerX){
+	        			hoveredBar = currentBar;
+	        			hoveredBarColorScheme = colorSchemes[i];
+	        			break;
+	        		}
+    			}
+        	}
+    	}
+    	var colorScheme = heatMap.getColorMapManager().getColorMap(hoveredBarColorScheme);
+    	var value = classBars[hoveredBar].values[pos-1];
+    	var colors = colorScheme.getColors();
+    	var classType = colorScheme.getType();
+    	if (value == 'null') {
+        	value = "Missing Value";
+    	}
+    	var thresholds = colorScheme.getThresholds();
+    	var thresholdSize = 0;
+    	// For Continuous Classifications: 
+    	// 1. Retrieve continuous threshold array from colorMapManager
+    	// 2. Retrieve threshold range size divided by 2 (1/2 range size)
+    	// 3. If remainder of half range > .75 set threshold value up to next value, Else use floor value.
+    	if (classType == 'continuous') {
+    		thresholds = colorScheme.getContinuousThresholdKeys();
+    		var threshSize = colorScheme.getContinuousThresholdKeySize()/2;
+    		if ((threshSize%1) > .5) {
+    			// Used to calculate modified threshold size for all but first and last threshold
+    			// This modified value will be used for color and display later.
+    			thresholdSize = Math.floor(threshSize)+1;
+    		} else {
+    			thresholdSize = Math.floor(threshSize);
+    		}
+    	}
+    	
+    	// Build TABLE HTML for contents of help box
+    	var helpContents = document.createElement("TABLE");
+    	setTableRow(helpContents, ["Class: ", "&nbsp;"+hoveredBar]);
+    	setTableRow(helpContents, ["Value: ", "&nbsp;"+value]);
+    	helpContents.insertRow().innerHTML = formatBlankRow();
+    	var rowCtr = 3 + thresholds.length;
+    	var prevThresh = currThresh;
+    	for (var i = 0; i < thresholds.length; i++){ // generate the color scheme diagram
+        	var color = colors[i];
+        	var valSelected = 0;
+        	var valTotal = classBars[hoveredBar].values.length;
+        	var currThresh = thresholds[i];
+        	var modThresh = currThresh;
+        	if (classType == 'continuous') {
+        		// IF threshold not first or last, the modified threshold is set to the threshold value 
+        		// less 1/2 of the threshold range ELSE the modified threshold is set to the threshold value.
+        		if ((i != 0) &&  (i != thresholds.length - 1)) {
+        			modThresh = currThresh - thresholdSize;
+        		}
+				color = colorScheme.getRgbToHex(colorScheme.getClassificationColor(modThresh));
+        	}
+        	
+        	//Count classification value occurrences within each breakpoint.
+        	for (var j = 0; j < valTotal; j++) {
+        		classBarVal = classBars[hoveredBar].values[j];
+        		if (classType == 'continuous') {
+            		// Count based upon location in threshold array
+            		// 1. For first threshhold, count those values <= threshold.
+            		// 2. For second threshold, count those values >= threshold.
+            		// 3. For penultimate threshhold, count those values > previous threshold AND values < final threshold.
+            		// 3. For all others, count those values > previous threshold AND values <= final threshold.
+        			if (i == 0) {
+						if (classBarVal <= currThresh) {
+       						valSelected++;
+						}
+        			} else if (i == thresholds.length - 1) {
+        				if (classBarVal >= currThresh) {
+        					valSelected++;
+        				}
+        			} else if (i == thresholds.length - 2) {
+		        		if ((classBarVal > prevThresh) && (classBarVal < currThresh)) {
+		        			valSelected++;
+		        		}
+        			} else {
+		        		if ((classBarVal > prevThresh) && (classBarVal <= currThresh)) {
+		        			valSelected++;
+		        		}
+        			}
+        		} else {
+                	var value = thresholds[i];
+	        		if (classBarVal == value) {
+	        			valSelected++;
+	        		}
+        		}
+        	}
+        	var selPct = Math.round(((valSelected / valTotal) * 100) * 100) / 100;  //new line
+        	setTableRow(helpContents, ["<div class='input-color'><div class='color-box' style='background-color: " + color + ";'></div></div>", modThresh + " (n = " + valSelected + ", " + selPct+ "%)"]);
+        	prevThresh = currThresh;
+    	}
+    	var valSelected = 0;  
+    	var valTotal = classBars[hoveredBar].values.length; 
+    	for (var j = 0; j < valTotal; j++) { 
+    		if (classBars[hoveredBar].values[j] == "null") { 
+    			valSelected++;  
+    		} 
+    	} 
+    	var selPct = Math.round(((valSelected / valTotal) * 100) * 100) / 100;  //new line
+    	setTableRow(helpContents, ["<div class='input-color'><div class='color-box' style='background-color: " +  colorScheme.getMissingColor() + ";'></div></div>", "Missing Color (n = " + valSelected + ", " + selPct+ "%)"]);
+        helptext.style.display="inherit";
+    	helptext.appendChild(helpContents);
+    	locateHelpBox(e, helptext);
+    } else {  // on the blank area in the top left corner
+    }
+    
+}
+	
+/**********************************************************************************
+ * FUNCTION - locateHelpBox: The purpose of this function is to set the location 
+ * for the display of a pop-up help panel based upon the cursor location and the
+ * size of the panel.
+ **********************************************************************************/
+function locateHelpBox(e, helptext) {
+    var rowClassWidthPx = getRowClassPixelWidth();
+    var colClassHeightPx = getColClassPixelHeight();
+	var mapLocY = e.layerY - colClassHeightPx;
+	var mapLocX = e.layerX - rowClassWidthPx;
+	var mapH = e.target.clientHeight - colClassHeightPx;
+	var mapW = e.target.clientWidth - rowClassWidthPx;
+	var boxLeft = e.pageX;
+	if (mapLocX > (mapW / 2)) {
+		boxLeft = e.pageX - helptext.clientWidth - 10;
+	}
+	helptext.style.left = boxLeft;
+	var boxTop = e.pageY;
+	if ((boxTop+helptext.clientHeight) > e.target.clientHeight + 90) {
+		boxTop = e.pageY - helptext.clientHeight;
+	}
+	helptext.style.top = boxTop;
+}
+
+/**********************************************************************************
+ * FUNCTION - detailDataToolHelp: The purpose of this function is to generate a 
+ * pop-up help panel for the tool buttons at the top of the detail pane. It receives
+ * text from chm.html. If the screen has been split, it changes the test for the 
+ * split screen button
+ **********************************************************************************/
+function detailDataToolHelp(e,text,width) {
+	userHelpClose();
+	detailPoint = setTimeout(function(){
+		if (typeof width === "undefined") {
+			width=50;
+		}
+		if ((isSub) && (text == "Split Into Two Windows")) {
+			text = "Join Screens";
+		}
+	    var helptext = getDivElement("helptext");
+	    helptext.style.position = "absolute";
+	    document.getElementsByTagName('body')[0].appendChild(helptext);
+	    if (text === "Modify Map Preferences") {
+	    	helptext.style.left = e.offsetLeft - 125;
+	    	
+	    } else {
+	    	helptext.style.left = e.offsetLeft + 15;
+	    }
+	    helptext.style.top = e.offsetTop + 15;
+	    helptext.style.width = width;
+		var htmlclose = "</font></b>";
+		helptext.innerHTML = "<b><font size='2' color='#0843c1'>"+text+"</font></b>";
+		helptext.style.display="inherit";
+	},1000);
+}
+
+/**********************************************************************************
+ * FUNCTION - getDivElement: The purpose of this function is to create and 
+ * return a DIV html element that is configured for a help pop-up panel.
+ **********************************************************************************/
+function getDivElement(elemName) {
+    var divElem = document.createElement('div');
+    divElem.id = elemName;
+    divElem.style.backgroundColor = 'CBDBF6'; 
+    divElem.style.display="none";
+    return divElem;
+}
+
+/**********************************************************************************
+ * FUNCTION - setTableRow: The purpose of this function is to set a row into a help
+ * or configuration html TABLE item for a given help pop-up panel. It receives text for 
+ * the header column, detail column, and the number of columns to span as inputs.
+ **********************************************************************************/
+function setTableRow(tableObj, tdArray, colSpan, align) {
+	var tr = tableObj.insertRow();
+	for (var i = 0; i < tdArray.length; i++) {
+		var td = tr.insertCell(i);
+		if (typeof colSpan != 'undefined') {
+			td.colSpan = colSpan;
+		}
+		if (i === 0) {
+			td.style.fontWeight="bold";
+		}
+		td.innerHTML = tdArray[i];
+		if (typeof align != 'undefined') {
+			td.align = align;
+		}
+	}
+}
+
+/**********************************************************************************
+ * FUNCTION - setTableRow: The purpose of this function is to set a row into a help
+ * or configuration html TABLE item for a given help pop-up panel. It receives text for 
+ * the header column, detail column, and the number of columns to span as inputs.
+ **********************************************************************************/
+function setErrorRow(tableObj, errorMsg) {
+	var tr = tableObj.insertRow();
+	var td = tr.insertCell(0);
+	td.colSpan = 2;
+	td.align="center";
+	td.style.fontWeight="bold";
+	td.style.color="red";
+	td.innerHTML = errorMsg;
+}
+
+
+
+/**********************************************************************************
+ * FUNCTION - formatBlankRow: The purpose of this function is to return the html
+ * text for a blank row.
+ **********************************************************************************/
+function formatBlankRow() {
+	return "<td style='line-height:4px;' colspan=2>&nbsp;</td>";
+}
+
+/**********************************************************************************
+ * FUNCTION - userHelpClose: The purpose of this function is to close any open 
+ * user help pop-ups and any active timeouts associated with those pop-up panels.
+ **********************************************************************************/
+function userHelpClose(){
+	clearTimeout(detailPoint);
+	var helptext = document.getElementById('helptext');
+	if (helptext){
+		helptext.remove();
+	}
+}
+
+
+//============================
+// LABEL MENU FUNCTIONS START
+//============================
+
+var linkouts = {};
+
+function createLabelMenus(){
+	createLabelMenu('Column'); // create the menu divs
+	createLabelMenu('ColumnClass');
+	createLabelMenu('Row');
+	createLabelMenu('RowClass');
+	getDefaultLinkouts();
+	populateLabelMenus(); // fill the divs with the appropriate linkouts
+}
+
+function labelHelpClose(axis){
+	var labelMenu = document.getElementById(axis + 'LabelMenu');
+    if (labelMenu){
+    	labelMenu.style.display = 'none';
+    }
+}
+
+function labelHelpOpen(axis, e){
+	var labelMenu = document.getElementById(axis + 'LabelMenu');
+	var labelMenuTable = document.getElementById(axis + 'LabelMenuTable');
+    if (labelMenu){
+    	labelMenu.style.display = 'inherit';
+    	labelMenu.style.left = e.x + labelMenu.offsetWidth > window.innerWidth ? window.innerWidth-labelMenu.offsetWidth : e.x;
+    	labelMenu.style.top = e.y + labelMenu.offsetHeight > window.innerHeight ? window.innerHeight-labelMenu.offsetHeight : e.y;
+    }
+    var axisLabelsLength = getSearchLabelsByAxis(axis).length;
+    var header = labelMenu.getElementsByClassName('labelMenuHeader')[0];
+    var row = header.getElementsByTagName('TR')[0];
+    if (axisLabelsLength > 0){
+    	row.innerHTML = "Selected labels : " + axisLabelsLength;
+    	labelMenuTable.getElementsByTagName("TBODY")[0].style.display = 'inherit';
+    } else {
+    	row.innerHTML = "Please select a label";
+    	labelMenuTable.getElementsByTagName("TBODY")[0].style.display = 'none';
+    }
+    
+}
+
+function createLabelMenu(axis){ // creates the divs for the label menu
+	var labelMenu = getDivElement(axis + 'LabelMenu');
+	document.body.appendChild(labelMenu);
+	labelMenu.style.position = 'absolute';
+	labelMenu.classList.add('labelMenu');
+	var topDiv = document.createElement("DIV");
+	topDiv.classList.add("labelMenuCaption");
+	topDiv.innerHTML = axis + ' Label Menu:';
+	var closeMenu = document.createElement("IMG");
+	closeMenu.src = staticPath + "images/closeButton.png";
+	closeMenu.classList.add('labelMenuClose')
+	closeMenu.addEventListener('click', function(){labelHelpClose(axis)},false);
+	var table = document.createElement("TABLE");
+	table.id = axis + 'LabelMenuTable';
+	var tableHead = table.createTHead();
+	tableHead.classList.add('labelMenuHeader');
+	var row = tableHead.insertRow();
+	labelMenu.appendChild(topDiv);
+	labelMenu.appendChild(table);
+	labelMenu.appendChild(closeMenu);
+	var tableBody = table.createTBody();
+	tableBody.classList.add('labelMenuBody');
+	var labelHelpCloseAxis = function(){ labelHelpClose(axis)};
+    document.addEventListener('click', labelHelpCloseAxis);
+}
+
+
+function populateLabelMenus(){ // adds the row linkouts and the column linkouts to the menus
+	var table = document.getElementById('RowLabelMenuTable');
+	var labelType = heatMap.getRowLabels()["label_type"];
+	for (i = 0; i < linkouts[labelType].length; i++)
+		addMenuItemToTable("Row", table, linkouts[labelType][i]);
+	
+	table = document.getElementById('ColumnLabelMenuTable');
+	labelType = heatMap.getColLabels()["label_type"];
+	for (i = 0; i < linkouts[labelType].length; i++)
+		addMenuItemToTable("Column", table, linkouts[labelType][i]);
+	
+	table = document.getElementById('ColumnClassLabelMenuTable');
+	labelType = 'ColumnClass';
+	for (i = 0; i < linkouts[labelType].length; i++)
+		addMenuItemToTable("ColumnClass", table, linkouts[labelType][i]);
+	
+	table = document.getElementById('RowClassLabelMenuTable');
+	labelType = 'RowClass';
+	for (i = 0; i < linkouts[labelType].length; i++)
+		addMenuItemToTable("RowClass", table, linkouts[labelType][i]);
+}
+
+function addMenuItemToTable(axis, table, linkout){
+	var body = table.getElementsByClassName('labelMenuBody')[0];
+	var row = body.insertRow();
+	var cell = row.insertCell();
+	cell.innerHTML = linkout.title;
+	
+	function functionWithParams(){ // this is the function that gets called when the linkout is clicked
+		var input;
+		switch (linkout.inputType){ // TO DO: make the function input types (ie: labels, index, etc) global constants. Possibly add more input types?
+			case "labels": input = getSearchLabelsByAxis(axis); break;
+//			case "index": input = axis.toUpperCase() == "ROW" ? getSearchRows() : getSearchCols();break;
+			default: input = axis.toUpperCase() == "ROW" ? getSearchRows() : getSearchCols();break;
+		}
+		linkout.callback(input,axis); // all linkout functions will have these inputs!
+	};
+	cell.addEventListener('click', functionWithParams);
+}
+
+function getDefaultLinkouts(){
+	addLinkout("Copy " + heatMap.getColLabels()["label_type"] +" to Clipboard", heatMap.getColLabels()["label_type"], "labels", copyToClipBoard,0);
+	addLinkout("Copy " +heatMap.getRowLabels()["label_type"] + " to Clipboard", heatMap.getRowLabels()["label_type"], "labels", copyToClipBoard,0);
+	addLinkout("Copy bar data for all labels to Clipboard", "ColumnClass", "labels",copyEntireClassBarToClipBoard,0);
+	addLinkout("Copy bar data for selected labels to Clipboard", "ColumnClass", "labels",copyPartialClassBarToClipBoard,1);
+	addLinkout("Copy bar data for all labels to Clipboard", "RowClass", "labels",copyEntireClassBarToClipBoard,0);
+	addLinkout("Copy bar data for selected labels to Clipboard", "RowClass", "labels",copyPartialClassBarToClipBoard,1);
+}
+
+function linkout (title, inputType, callback){ // the linkout object
+	this.title = title;
+	this.inputType = inputType; // input type of the callback function
+	this.callback = callback;
+}
+
+function addLinkout(name, labelType, inputType, callback, index){ // adds linkout objects to the linkouts global variable
+	if (!linkouts[labelType]){
+		linkouts[labelType] = [new linkout(name, inputType,callback)];
+	} else {
+		if (index !== undefined){
+			linkouts[labelType].splice(index, 0, new linkout(name,inputType,callback)); 
+		}else {
+			linkouts[labelType].push(new linkout(name,inputType,callback));
+		}
+	}
+}
+
+function copyToClipBoard(labels,axis){
+	window.open("","",'width=335,height=330,resizable=1').document.write(labels.join(", "));
+}
+
+function copyEntireClassBarToClipBoard(labels,axis){
+	var newWindow = window.open("","",'width=335,height=330,resizable=1');
+	var newDoc = newWindow.document;
+	var axisLabels = axis == "ColumnClass" ? heatMap.getColLabels()["labels"] : heatMap.getRowLabels()["labels"]; 
+	var classifications = heatMap.getClassifications();
+	newDoc.write("Sample&emsp;" + labels.join("&emsp;") + ":<br>");
+	for (var i = 0; i < axisLabels.length; i++){
+		newDoc.write(axisLabels[i] + "&emsp;");
+		for (var j = 0; j < labels.length; j++){
+			newDoc.write(classifications[labels[j]].values[i] + "&emsp;");
+		}
+		newDoc.write("<br>");
+	}
+}
+
+function copyPartialClassBarToClipBoard(labels,axis){
+	var newWindow = window.open("","",'width=335,height=330,resizable=1');
+	var newDoc = newWindow.document;
+	var axisLabels = axis == "ColumnClass" ? getSearchLabelsByAxis("Column") : getSearchLabelsByAxis("Row");
+	var labelIndex = axis == "ColumnClass" ? getSearchCols() : getSearchRows(); 
+	var classifications = heatMap.getClassifications();
+	newDoc.write("Sample&emsp;" + labels.join("&emsp;") + ":<br>");
+	for (var i = 0; i < axisLabels.length; i++){
+		newDoc.write(axisLabels[i] + "&emsp;");
+		for (var j = 0; j < labels.length; j++){
+			newDoc.write(classifications[labels[j]].values[labelIndex[i]-1] + "&emsp;");
+		}
+		newDoc.write("<br>");
+	}
+}
+
+//===========================
+// LABEL MENU FUNCTIONS END 
+//===========================
+
+function showSearchError(type){
+	var searchError = getDivElement('searchError');
+	searchError.style.display = 'inherit';
+	var searchBar = document.getElementById('search_text');
+	searchError.style.top = searchBar.offsetTop + searchBar.offsetHeight;
+	searchError.style.left = searchBar.offsetLeft + searchBar.offsetWidth;
+	switch (type){
+		case 0: searchError.innerHTML = "No matching labels found"; break;
+		case 1: searchError.innerHTML = "Exit dendrogram selection to go to " + currentSearchItem.label;break;
+		case 2: searchError.innerHTML = "All " + currentSearchItem.axis +  " items are visible. Change the view mode to see " + currentSearchItem.label;break;
+	}
+	document.body.appendChild(searchError);
+	setTimeout(function(){
+		searchError.remove();
+	}, 2000);
+	
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mda_heatmap_viz/static/javascript/UserPreferenceManager.js	Thu Apr 07 14:43:37 2016 -0400
@@ -0,0 +1,1159 @@
+/**********************************************************************************
+ * USER PREFERENCE FUNCTIONS:  The following functions handle the processing 
+ * for user preference editing. 
+ **********************************************************************************/
+
+//Global variables for preference processing
+var maxRows = 0;
+var helpRowSize = 0;
+var bkpColorMap = null;
+var filterVal;
+var searchPerformed = false;
+
+/*===================================================================================
+ *  COMMON PREFERENCE PROCESSING FUNCTIONS
+ *  
+ *  The following functions are utilized to present the entire heat map preferences
+ *  dialog and, therefore, sit above those functions designed specifically for processing
+ *  individual data layer and covariate classification bar preferences:
+ *  	- editPreferences
+ *  	- setPrefsDivSizing
+ *  	- showLayerPrefs
+ *      - showClassPrefs
+ *      - showRowsColsPrefs
+ *      - prefsCancel
+ *      - prefsApply
+ *      - prefsValidate
+ *      - prefsValidateBreakPoints
+ *      - prefsValidateBreakColors
+ *      - prefsApplyBreaks
+ *      - getNewBreakColors
+ *      - getNewBreakThresholds  
+ *      - prefsSave
+ =================================================================================*/
+
+/**********************************************************************************
+ * FUNCTION - editPreferences: This is the MAIN driver function for edit 
+ * preferences processing.  It is called under two conditions (1) The Edit 
+ * preferences "gear" button is pressed on the main application screen 
+ * (2) User preferences have been applied BUT errors have occurred.
+ * 
+ * Processing for this function is documented in detail in the body of the function.
+ **********************************************************************************/
+function editPreferences(e,errorMsg){
+	maxRows = 0;
+	userHelpClose();
+
+	// If helpPrefs element already exists, the user is pressing the gear button
+	// when preferences are already open. Disregard.
+	var helpExists = document.getElementById('prefsPanel');
+	if ((isSub) || (helpExists !== null)) {
+		return;
+	}
+
+	//If first time thru, save the dataLayer colorMap
+	//This is done because the colorMap must be edited to add/delete breakpoints while retaining their state
+	var colorMap = heatMap.getColorMapManager().getColorMap("dl1"); //TODO - Modify when multiple data layers (flick) are added
+	if (bkpColorMap === null) {
+		bkpColorMap = colorMap;
+	} 
+
+	//Create a "master" DIV for displaying edit preferences
+	var prefspanel = getDivElement("prefsPanel");
+	document.getElementsByTagName('body')[0].appendChild(prefspanel);
+	
+	//Create a one cell table and populate the header and tab elements in the first 3 table rows
+	var prefContents = document.createElement("TABLE");
+	var headDiv = document.createElement('div');
+	headDiv.className = 'prefsHeader';
+	headDiv.id = 'prefsHeader';
+	prefspanel.appendChild(headDiv);
+	headDiv.textContent = 'Heat Map Display Properties';
+	
+	prefContents.insertRow().innerHTML = formatBlankRow();
+	prefContents.insertRow().innerHTML = "<td style='line-height:30px;'>&nbsp;</td>";
+	prefContents.insertRow().innerHTML = "<td style='border-bottom-style:solid;border-bottom-width:2px;position: relative;'><div id='prefTab_buttons' style='position: absolute; bottom: 0;' align='left'><img id='prefRowsCols_btn' src='" + staticPath + "rowsColsOn.png' alt='Edit Rows & Columns' onclick='showRowsColsPrefs();' align='top'/>&nbsp;<img id='prefLayer_btn' src='" + staticPath + "images/dataLayersOff.png' alt='Edit Data Layers' onclick='showLayerPrefs();' align='top'/>&nbsp;<img id='prefClass_btn' src='" + staticPath+ "images/covariateBarsOff.png' alt='Edit Classifications' onclick='showClassPrefs();' align='top'/></div></td>";
+	//Initialize rowCtr variable
+	var rowCtr = 3;
+
+	//Create a parent DIV as a container for breakpoint and classification edit display
+	var prefprefs = getDivElement("prefprefs"); 
+
+	//Create and populate row & col preferences DIV and add to parent DIV
+	var rowcolprefs = setupRowColPrefs(e, prefprefs);
+	rowcolprefs.style.display="none";
+	prefprefs.appendChild(rowcolprefs);
+
+	//Create and populate classifications preferences DIV and add to parent DIV
+	var classprefs = setupClassPrefs(e, prefprefs);
+	classprefs.style.display="none";
+	prefprefs.appendChild(classprefs);
+	
+	//Create and populate breakpoint preferences DIV and add to parent DIV
+	var layerprefs = setupLayerPrefs(e, prefprefs);
+	layerprefs.style.display="none";
+	prefprefs.appendChild(layerprefs);
+
+	// Set DIV containing both class and break DIVs to visible and append to prefspanel table
+	prefprefs.style.display="block";
+	var row1 = prefContents.insertRow();
+	var row1Cell = row1.insertCell(0);
+	row1Cell.appendChild(prefprefs);
+	
+	//If error message exists add table row to prefspanel table containing error message
+	if (errorMsg != null) {
+		setErrorRow(prefContents, errorMsg[2]);
+	}
+
+	var prefButtons = document.createElement("TABLE");
+	//Add Cancel, Apply, and Save buttons to bottom of prefspanel table
+	var buttons = "<img id='prefCancel_btn' src='" + staticPath + "images/prefCancel.png' alt='Cancel changes' onclick='prefsCancelButton();' align='top'/>&nbsp;<img id='prefApply_btn' src='" + staticPath + "images/prefApply.png' alt='Apply changes' onclick='prefsApplyButton();' align='top'/>";
+	if (heatMap.isSaveAllowed()) {
+		buttons = buttons + "&nbsp;<img id='prefSave_btn' src='" + staticPath + "images/prefSave.png' alt='Save changes' onclick='prefsSaveButton();' align='top'/>";
+	}
+	setTableRow(prefButtons,["<div id='pref_buttons' align='right'>"+buttons+"</div>"]);
+	rowCtr++;
+	prefprefs.appendChild(prefButtons);
+
+	//Add prefspanel table to the main preferences DIV and set position and display
+	prefspanel.appendChild(prefContents);
+    prefspanel.style.position = "absolute";
+	prefspanel.style.top = e.offsetTop + 15;
+	prefspanel.style.display="inherit";
+
+	//maxRows has been loaded with a count of the datalayer/class panel with the most rows
+	//add to this the number of rows added during construction of prefspanel table.
+	maxRows = maxRows+rowCtr;
+	//Retrieve maximum row height size used in various preferences DIVs
+	helpRowSize = parseFloat(getStyle(prefspanel, 'font-size' ), 10)*1.45;
+	//Use the two above numbers to apply sizing to all of the preferences DIVs.
+	setPrefsDivSizing();
+	prefspanel.style.left = e.offsetLeft - parseInt(layerprefs.style.width,10);
+	
+	//If errors exist and they are NOT on the currently visible DIV (dataLayer1),
+	//hide the dataLayers DIV, set the tab to "Covariates", and open the appropriate
+	//covariate bar DIV.
+	showDendroSelections();
+	setShowAll();
+	if ((errorMsg != null) && (errorMsg[1] === "classPrefs")) {
+		showClassBreak(errorMsg[0]);
+		showClassPrefs();
+	} else if ((errorMsg != null) && (errorMsg[1] === "layerPrefs")){ 
+		showLayerPrefs();
+	} else if (searchPerformed){ 
+		searchPerformed = false;
+		showClassPrefs();
+	} else {
+		showRowsColsPrefs();
+	}
+
+}
+
+/**********************************************************************************
+ * FUNCTION - setPrefsDivSizing: The purpose of this function is to resize the
+ * various DIVs inside the preferences dialog.  It is called when the dialog is 
+ * first opened and whenever data layer breakpoints are added (if necessary).
+ **********************************************************************************/
+//TODO - This can be improved as the current sizing is not exact.
+function setPrefsDivSizing() {
+	var rowsColsprefs = document.getElementById("rowsColsPrefs");
+	var layerprefs = document.getElementById("layerPrefs");
+	var classprefs = document.getElementById("classPrefs");
+	var helprefs = document.getElementById("prefsPanel");
+	var prefHeight = maxRows*helpRowSize;
+	var prefWidth = 380;
+	rowsColsprefs.style.width = prefWidth;
+	rowsColsprefs.style.height = prefHeight;
+	layerprefs.style.width = prefWidth;
+	layerprefs.style.height = prefHeight;
+	classprefs.style.width = prefWidth;
+	classprefs.style.height = prefHeight;
+}
+
+/**********************************************************************************
+ * FUNCTION - showLayerPrefs: The purpose of this function is to perform the 
+ * processing for the preferences tab when the user selects the "Data Layers" tab.
+ **********************************************************************************/
+function showRowsColsPrefs() {
+	//Turn off all tabs
+	hideAllPrefs();
+	//Turn on layer prefs tab
+	var rowsColsBtn = document.getElementById("prefRowsCols_btn");
+	rowsColsBtn.setAttribute('src', staticPath + 'images/rowsColsOn.png');
+	var rowsColsDiv = document.getElementById("rowsColsPrefs");
+	rowsColsDiv.style.display="block";
+}
+
+
+/**********************************************************************************
+ * FUNCTION - showLayerPrefs: The purpose of this function is to perform the 
+ * processing for the preferences tab when the user selects the "Data Layers" tab.
+ **********************************************************************************/
+function showLayerPrefs() {
+	//Turn off all tabs
+	hideAllPrefs();
+	//Turn on layer prefs tab
+	var layerBtn = document.getElementById("prefLayer_btn");
+	layerBtn.setAttribute('src', staticPath + 'images/dataLayersOn.png');
+	var layerDiv = document.getElementById("layerPrefs");
+	layerDiv.style.display="block";
+}
+
+/**********************************************************************************
+ * FUNCTION - showClassPrefs: The purpose of this function is to perform the 
+ * processing for the preferences tab when the user selects the "Covariates" tab.
+ **********************************************************************************/
+function showClassPrefs() {
+	//Turn off all tabs
+	hideAllPrefs();
+	//Turn on classification prefs tab
+	var classBtn = document.getElementById("prefClass_btn");
+	classBtn.setAttribute('src', staticPath + 'images/covariateBarsOn.png');
+	var classDiv = document.getElementById("classPrefs");
+	classDiv.style.display="block";
+}
+
+/**********************************************************************************
+ * FUNCTION - hideAllPrefs: The purpose of this function is to set all tabs off. It 
+ * is called whenever a tab is clicked.  All tabs are set to hidden with their
+ * image set to the "off" image.
+ **********************************************************************************/
+function hideAllPrefs() {
+	var classBtn = document.getElementById("prefClass_btn");
+	classBtn.setAttribute('src', staticPath + 'images/covariateBarsOff.png');
+	var classDiv = document.getElementById("classPrefs");
+	classDiv.style.display="none";
+	var layerBtn = document.getElementById("prefLayer_btn");
+	layerBtn.setAttribute('src', staticPath + 'images/dataLayersOff.png');
+	var layerDiv = document.getElementById("layerPrefs");
+	layerDiv.style.display="none";
+	var rowsColsBtn = document.getElementById("prefRowsCols_btn");
+	rowsColsBtn.setAttribute('src', staticPath + 'images/rowsColsOff.png');
+	var rowsColsDiv = document.getElementById("rowsColsPrefs");
+	rowsColsDiv.style.display="none";
+}
+
+/**********************************************************************************
+ * FUNCTION - prefsCancelButton: The purpose of this function is to perform all processing
+ * necessary to exit the user preferences dialog WITHOUT applying or saving any 
+ * changes made by the user when the Cancel button is pressed on the ColorMap 
+ * preferences dialog.  Since the dataLayer colormap must be edited to add/delete
+ * breakpoints, the backup colormap (saved when preferences are first opened) is re-
+ * applied to the colorMapManager.  Then the preferences DIV is retrieved and removed.
+ **********************************************************************************/
+function prefsCancelButton() {
+	if (bkpColorMap !== null) {
+		var colorMapMgr = heatMap.getColorMapManager();
+		colorMapMgr.setColorMap("dl1", bkpColorMap);
+	}
+	var prefspanel = document.getElementById('prefsPanel');
+	if (prefspanel){
+		prefspanel.remove();
+	}
+}
+
+/**********************************************************************************
+ * FUNCTION - prefsApplyButton: The purpose of this function is to perform all processing
+ * necessary to reconfigure the "current" presentation of the heat map in the 
+ * viewer when the Apply button is pressed on the ColorMap Preferences Dialog.  
+ * First validations are performed.  If errors are found, preference 
+ * changes are NOT applied and the user is re-presented with the preferences dialog
+ * and the error found.  If no errors are found, all changes are applied to the heatmap 
+ * and the summary panel, detail panel, and covariate bars are redrawn.  However, 
+ * these changes are not yet permanently  saved to the JSON files that are used to 
+ * configure heat map presentation.
+ **********************************************************************************/
+function prefsApplyButton() {
+	//Perform validations of all user-entered data layer and covariate bar
+	//preference changes.
+	var errorMsg = prefsValidate();
+	prefsApply();
+	if (errorMsg !== null) {
+		prefsError(errorMsg);
+	} else { 
+		prefsSuccess();
+	}
+}
+
+/**********************************************************************************
+ * FUNCTION - prefsSaveButton: The purpose of this function is to perform all processing
+ * necessary to permanently save user preference changes.  This will result in 
+ * changes to the JSON files that are used to configure heat map presentation.
+ **********************************************************************************/
+function prefsSaveButton() {
+	var mode = heatMap.mode;
+	var errorMsg = prefsValidate();
+	prefsApply();
+	if (errorMsg !== null) {
+		prefsError(errorMsg);
+	} else { 
+		var success = heatMap.saveHeatMapProperties();
+		if (success === "false") {
+			prefsError(["dl1", "layerPrefs", "ERROR: Preferences failed to save. Use Apply or Cancel to continue."]);
+		} else {
+			prefsSuccess();
+		}
+	}
+}
+
+/**********************************************************************************
+ * FUNCTION - prefsSuccess: The purpose of this function perform the functions
+ * necessary when preferences are determined to be valid. It is shared by the
+ * Apply and Save buttons.
+ **********************************************************************************/
+function prefsSuccess() {
+	filterVal = null;
+	//Remove the backup color map (used to reinstate colors if user cancels)
+	//and formally apply all changes to the heat map, re-draw, and exit preferences.
+	bkpColorMap = null;
+	summaryInit();
+	detailInit();
+	changeMode('NORMAL');
+	prefsCancelButton();
+}
+
+/**********************************************************************************
+ * FUNCTION - prefsError: The purpose of this function perform the functions
+ * necessary when preferences are determined to be invalid. It is shared by the
+ * Apply and Save buttons.
+ **********************************************************************************/
+function prefsError(errorMsg) {
+	//If a validation error exists, re-present the user preferences
+	//dialog with the error message displayed in red. 
+	var prefspanel = document.getElementById('prefsPanel');
+	if (prefspanel){
+		prefspanel.remove();
+	}
+	editPreferences(document.getElementById('gear_btn'),errorMsg);
+}
+
+/**********************************************************************************
+ * FUNCTION - prefsSuccess: The purpose of this function is to apply all user
+ * ColorMap preferences settings.  It is shared by the Apply and Save buttons.
+ **********************************************************************************/
+function prefsApply() {
+	// Apply Row & Column Preferences
+	var dendrogram = heatMap.getDendrogram();
+	var rowLabels = heatMap.getRowLabels();
+	var rowOrder = rowLabels['order_method'];
+	if (rowOrder === "Hierarchical") {
+		var rowDendroShowVal = document.getElementById("rowDendroShowPref").value;
+		dendrogram['row_dendro_show'] = rowDendroShowVal;
+		dendrogram['row_dendro_height'] = document.getElementById("rowDendroHeightPref").value;
+	}	
+	var colLabels = heatMap.getColLabels();
+	var colOrder = colLabels['order_method'];
+	if (colOrder === "Hierarchical") {
+		var colDendroShowVal = document.getElementById("colDendroShowPref").value;
+		dendrogram['col_dendro_show'] = colDendroShowVal;
+		dendrogram['col_dendro_height'] = document.getElementById("colDendroHeightPref").value;
+	}	
+	// Apply Covariate Bar Preferences
+	var classBars = heatMap.getClassifications();
+	for (var key in classBars){
+		var showElement = document.getElementById(key+"_showPref");
+		var heightElement = document.getElementById(key+"_heightPref");
+		if (filterShow(key)) {
+			heatMap.setClassificationPrefs(key,showElement.checked,heightElement.value);
+		} else {
+			heatMap.setClassificationPrefs(key,false,15);
+		}
+		prefsApplyBreaks(classBars[key].colorScheme,"covariate",filterShow(key));
+	}
+	// Apply Data Layer Preferences
+	//TODO - Future loop for data layers
+	prefsApplyBreaks("dl1","datalayer",true);
+}
+
+/**********************************************************************************
+ * FUNCTION - prefsValidate: The purpose of this function is to validate all user
+ * changes to the heatmap properties. When the very first error is found, an error 
+ * message (string array containing error information) is created and returned to 
+ * the prefsApply function. 
+ **********************************************************************************/
+function prefsValidate() {
+	var classBars = heatMap.getClassifications();
+	var errorMsg = null;
+	//Loop thru all covariate classfication bars validating all break colors
+	for (var key in classBars){
+		if (filterShow(key)) {
+			var showElement = document.getElementById(key+"_showPref");
+			var heightElement = document.getElementById(key+"_heightPref");
+			errorMsg = prefsValidateBreakColors(classBars[key].colorScheme,"classPrefs");
+			if (errorMsg !== null) break;
+		}
+	}
+	//Validate all breakpoints and colors for the main data layer
+	if (errorMsg === null) {
+		//TODO: currently only processing for data layer 1. This will require modification
+		// when new data layers (e.g. flicks) are added to the heatmap.
+		errorMsg = prefsValidateBreakPoints("dl1","layerPrefs");
+		if (errorMsg === null) {
+			errorMsg = prefsValidateBreakColors("dl1","layerPrefs");
+		}
+	}
+	return errorMsg;
+}
+
+/**********************************************************************************
+ * FUNCTION - prefsValidateBreakPoints: The purpose of this function is to validate 
+ * all user breakpoint and color changes to heatmap data layer properties. When the  
+ * first error is found, an error  message (string array containing error information) 
+ * is created and returned to the prefsApply function. 
+ **********************************************************************************/
+function prefsValidateBreakPoints(colorMapName,prefPanel) {
+	var colorMap = heatMap.getColorMapManager().getColorMap(colorMapName);
+	var thresholds = colorMap.getThresholds();
+	var colors = colorMap.getColors();
+	var dupeBreak = false;
+	var breakOrder = false;
+	var prevBreakValue = -99999;
+	var errorMsg = null;
+	//Loop thru colormap thresholds and validate for order and duplicates
+	for (var i = 0; i < thresholds.length; i++) {
+		var breakElement = document.getElementById(colorMapName+"_breakPt"+i+"_breakPref");
+		//If current breakpoint is not greater than previous, throw order error
+		if (parseInt(breakElement.value) < prevBreakValue) {
+			breakOrder = true;
+			break;
+		}
+		//Loop thru thresholds, skipping current element, searching for a match to the 
+		//current selection.  If found, throw duplicate error
+		for (var j = 0; j < thresholds.length; j++) {
+			var be = document.getElementById(colorMapName+"_breakPt"+j+"_breakPref");
+			if (i != j) {
+				if (breakElement.value === be.value) {
+					dupeBreak = true;
+					break;
+				}
+			}
+		}
+	}
+	if (breakOrder) {
+		errorMsg =  [colorMapName, prefPanel, "ERROR: Data layer breakpoints must be in order"];
+	}
+	if (dupeBreak) {
+		errorMsg =  [colorMapName, prefPanel, "ERROR: Duplicate data layer breakpoint found above"];
+	}
+	return errorMsg;
+}
+
+/**********************************************************************************
+ * FUNCTION - prefsValidateBreakColors: The purpose of this function is to validate 
+ * all user color changes to heatmap classification and data layer properties. When the  
+ * first error is found, an error  message (string array containing error information) 
+ * is created and returned to the prefsApply function. 
+ **********************************************************************************/
+function prefsValidateBreakColors(colorMapName,prefPanel) {
+	var colorMap = heatMap.getColorMapManager().getColorMap(colorMapName);
+	var thresholds = colorMap.getThresholds();
+	var colors = colorMap.getColors();
+	var dupeColor = false;
+	for (var i = 0; i < colors.length; i++) {
+		var colorElement = document.getElementById(colorMapName+"_color"+i+"_colorPref");
+		for (var j = 0; j < thresholds.length; j++) {
+			var ce = document.getElementById(colorMapName+"_color"+j+"_colorPref");
+			if (i != j) {
+				if (colorElement.value === ce.value) {
+					dupeColor = true;
+					break;
+				}
+			}
+		}
+	}
+	if (dupeColor) {
+		return [colorMapName, prefPanel, "ERROR: Duplicate color setting found above"];
+	}
+	return null;
+}
+
+/**********************************************************************************
+ * FUNCTION - prefsApplyBreaks: The purpose of this function is to apply all 
+ * user entered changes to colors and breakpoints. 
+ **********************************************************************************/
+function prefsApplyBreaks(colorMapName, colorMapType, show) {
+	var colorMap = heatMap.getColorMapManager().getColorMap(colorMapName);
+	if (show) {
+		var thresholds = colorMap.getThresholds();
+		var colors = colorMap.getColors();
+		var newColors = getNewBreakColors(colorMapName);
+		colorMap.setColors(newColors);
+		if (colorMapType === "datalayer") {
+			var newThresholds = getNewBreakThresholds(colorMapName);
+			colorMap.setThresholds(newThresholds);
+		}
+		var missingElement = document.getElementById(colorMapName+"_missing_colorPref");
+		colorMap.setMissingColor(missingElement.value);
+		var colorMapMgr = heatMap.getColorMapManager();
+		colorMapMgr.setColorMap(colorMapName, colorMap);
+	}
+}
+
+/**********************************************************************************
+ * FUNCTION - getNewBreakColors: The purpose of this function is to grab all user
+ * color entries for a given colormap and place them on a string array.  It will 
+ * iterate thru the screen elements, pulling the current color entry for each 
+ * element, placing it in a new array, and returning that array. This function is 
+ * called by the prefsApplyBreaks function.  It is ALSO called from the data layer
+ * addLayerBreak and deleteLayerBreak functions with parameters passed in for 
+ * the position to add/delete and the action to be performed (add/delete).
+ **********************************************************************************/
+function getNewBreakColors(colorMapName, pos, action) {
+	var colorMap = heatMap.getColorMapManager().getColorMap(colorMapName);
+	var thresholds = colorMap.getThresholds();
+	var newColors = [];
+	for (var j = 0; j < thresholds.length; j++) {
+		var colorElement = document.getElementById(colorMapName+"_color"+j+"_colorPref");
+		//If being called from addLayerBreak or deleteLayerBreak
+		if (typeof pos !== 'undefined') {
+			if (action === "add") {
+				newColors.push(colorElement.value);
+				if (j === pos) {
+					newColors.push(colorElement.value);
+				}
+			} else {
+				if (j !== pos) {
+					newColors.push(colorElement.value);
+				}
+			}
+		} else {
+			newColors.push(colorElement.value);
+		}
+	}
+	return newColors;
+}
+
+/**********************************************************************************
+ * FUNCTION - getNewBreakThresholds: The purpose of this function is to grab all user
+ * data layer breakpoint entries for a given colormap and place them on a string array.  
+ * It will  iterate thru the screen elements, pulling the current breakpoint entry for each 
+ * element, placing it in a new array, and returning that array. This function is 
+ * called by the prefsApplyBreaks function (only for data layers).  It is ALSO called 
+ * from the data layer addLayerBreak and deleteLayerBreak functions with parameters 
+ * passed in for the position to add/delete and the action to be performed (add/delete).
+ **********************************************************************************/
+function getNewBreakThresholds(colorMapName, pos, action) {
+	var colorMap = heatMap.getColorMapManager().getColorMap(colorMapName);
+	var thresholds = colorMap.getThresholds();
+	var newThresholds = [];
+	for (var j = 0; j < thresholds.length; j++) {
+		var breakElement = document.getElementById(colorMapName+"_breakPt"+j+"_breakPref");
+		if (typeof pos !== 'undefined') {
+			if (action === "add") {
+				newThresholds.push(breakElement.value);
+				if (j === pos) {
+					newThresholds.push(breakElement.value);
+				}
+			} else {
+				if (j !== pos) {
+					newThresholds.push(breakElement.value);
+				}
+			}
+		} else {
+			newThresholds.push(breakElement.value);
+		}
+	}
+	return newThresholds;
+}
+
+/*===================================================================================
+  *  DATA LAYER PREFERENCE PROCESSING FUNCTIONS
+  *  
+  *  The following functions are utilized to present heat map data layer 
+  *  configuration options:
+  *  	- setupLayerPrefs
+  *  	- setupLayerBreaks
+  *  	- setupLayerPrefs
+  *     - addLayerBreak
+  *     - deleteLayerBreak
+  *     - reloadLayerBreaksColorMap
+  =================================================================================*/
+
+/**********************************************************************************
+ * FUNCTION - setupLayerPrefs: The purpose of this function is to construct a DIV 
+ * panel containing all data layer preferences.  A dropdown list containing all 
+ * data layers is presented and individual DIVs for each data layer, containing 
+ * breakpoints/colors, are added.
+ **********************************************************************************/
+function setupLayerPrefs(e, prefprefs){
+	var layerprefs = getDivElement("layerPrefs");
+	var prefContents = document.createElement("TABLE");
+	var colorMapName = "dl1";
+	var colorMap = heatMap.getColorMapManager().getColorMap(colorMapName);
+	prefContents.insertRow().innerHTML = formatBlankRow();
+	// TODO Future: primary and flick data layers in dropdown
+	var dlSelect = "<select name='dlPref_list' id='dlPref_list' onchange='showDlBreak();'><option value='dl1'>Data Layer 1</option></select>"
+	setTableRow(prefContents,["Data Layer: ", dlSelect]);
+	prefContents.insertRow().innerHTML = formatBlankRow();
+	prefContents.insertRow().innerHTML = formatBlankRow();
+	layerprefs.appendChild(prefContents);
+	var breakprefs = setupLayerBreaks(e, colorMapName, colorMapName);
+	breakprefs.style.display="block";
+	breakprefs.style.width = 300;
+	layerprefs.appendChild(breakprefs);
+	maxRows = maxRows+3;
+	// TODO Future: loop for primary and flick data layers
+	return layerprefs;
+}
+
+/**********************************************************************************
+ * FUNCTION - setupLayerBreaks: The purpose of this function is to construct a DIV 
+ * containing a list of breakpoints/colors for a given matrix data layer.
+ **********************************************************************************/
+function setupLayerBreaks(e, mapName, barName, barType){
+	var classBars = heatMap.getClassifications();
+	var colorMap = heatMap.getColorMapManager().getColorMap(mapName);
+	var thresholds = colorMap.getThresholds();
+	var colors = colorMap.getColors();
+	var helpprefs = getDivElement("breakPrefs_"+mapName);
+	var prefContents = document.createElement("TABLE"); 
+	var rowCtr = 0;
+	prefContents.insertRow().innerHTML = formatBlankRow();
+	prefContents.insertRow().innerHTML = formatBlankRow();
+	rowCtr++;
+	setTableRow(prefContents, ["<u>Breakpoint</u>", "<b><u>"+"Color"+"</u></b>","&nbsp;"]); 
+	rowCtr++;
+	for (var j = 0; j < thresholds.length; j++) {
+		var threshold = thresholds[j];
+		var color = colors[j];
+		var threshId = mapName+"_breakPt"+j;
+		var colorId = mapName+"_color"+j;
+		var breakPtInput = "<input name='"+threshId+"_breakPref' id='"+threshId+"_breakPref' value='"+threshold+"' maxlength='4' size='4'>";
+		var colorInput = "<input class='spectrumColor' type='color' name='"+colorId+"_colorPref' id='"+colorId+"_colorPref' value='"+color+"'>"; 
+		var addButton = "<img id='"+threshId+"_breakAdd' src='" + staticPath + "images/plusButton.png' alt='Add Breakpoint' onclick='addLayerBreak("+j+",\""+mapName+"\");' align='top'/>"
+		var delButton = "<img id='"+threshId+"_breakDel' src='" + staticPath + "images/minusButton.png' alt='Remove Breakpoint' onclick='deleteLayerBreak("+j+",\""+mapName+"\");' align='top'/>"
+		if (j === 0) {
+			setTableRow(prefContents, [breakPtInput, colorInput, addButton]);
+		} else {
+			setTableRow(prefContents, [breakPtInput,  colorInput, addButton, delButton]);
+		}
+		rowCtr++;
+	} 
+	prefContents.insertRow().innerHTML = formatBlankRow();
+	rowCtr++;
+	setTableRow(prefContents, ["Missing Color:",  "<input class='spectrumColor' type='color' name='"+mapName+"_missing_colorPref' id='"+mapName+"_missing_colorPref' value='"+colorMap.getMissingColor()+"'>"]);
+	rowCtr++;
+	if (rowCtr > maxRows) {
+		maxRows = rowCtr;
+	}
+	helpprefs.style.height = rowCtr;
+	helpprefs.style.width = 30;
+	helpprefs.appendChild(prefContents);
+	return helpprefs;
+}	
+
+/**********************************************************************************
+ * FUNCTION - addLayerBreak: The purpose of this function is to add a breakpoint
+ * row to a data layer colormap. A new row is created using the preceding row as a 
+ * template (i.e. breakpt value and color same as row clicked on).  
+ **********************************************************************************/
+function addLayerBreak(pos,colorMapName) {
+	//Retrieve colormap for data layer
+	var colorMap = heatMap.getColorMapManager().getColorMap(colorMapName);
+	var newThresholds = getNewBreakThresholds(colorMapName, pos,"add");
+	var newColors = getNewBreakColors(colorMapName, pos,"add");
+	//Calculate new size of data layers panel and reset size of the 
+	// entire preferences dialog (if necessary)
+	var layerRows = newThresholds.length+helpRowSize;
+	maxRows = Math.max(maxRows,layerRows);
+	setPrefsDivSizing();
+	//Apply new arrays for thresholds and colors to the datalayer
+	//and reload the colormap.
+	colorMap.setThresholds(newThresholds);
+	colorMap.setColors(newColors);
+	reloadLayerBreaksColorMap(colorMapName, colorMap);
+}
+
+/**********************************************************************************
+ * FUNCTION - deleteLayerBreak: The purpose of this function is to remove a breakpoint
+ * row from a data layer colormap.   
+ **********************************************************************************/
+function deleteLayerBreak(pos,colorMapName) {
+	var colorMap = heatMap.getColorMapManager().getColorMap(colorMapName);
+	var thresholds = colorMap.getThresholds();
+	var colors = colorMap.getColors();
+	var newThresholds = getNewBreakThresholds(colorMapName, pos,"delete");
+	var newColors = getNewBreakColors(colorMapName, pos,"delete");
+	//Apply new arrays for thresholds and colors to the datalayer
+	//and reload the colormap.
+	colorMap.setThresholds(newThresholds);
+	colorMap.setColors(newColors);
+	reloadLayerBreaksColorMap(colorMapName, colorMap);
+}
+
+/**********************************************************************************
+ * FUNCTION - reloadLayerBreaksColorMap: The purpose of this function is to reload
+ * the colormap for a given data layer.  The add/deleteLayerBreak methods call
+ * this common function.  The layerPrefs DIV is retrieved and the setupLayerBreaks
+ * method is called, passing in the newly edited colormap. 
+ **********************************************************************************/
+function reloadLayerBreaksColorMap(colorMapName, colorMap) {
+	var e = document.getElementById('gear_btn')
+	var colorMapMgr = heatMap.getColorMapManager();
+	colorMapMgr.setColorMap(colorMapName, colorMap);
+	var breakPrefs = document.getElementById('breakPrefs_'+colorMapName);
+	if (breakPrefs){
+		breakPrefs.remove();
+	}
+	var layerprefs = getDivElement("layerPrefs");
+	var breakPrefs = setupLayerBreaks(e, colorMapName, colorMapName);
+	breakPrefs.style.display="block";
+	breakPrefs.style.width = 300;
+	layerPrefs.appendChild(breakPrefs);
+}
+
+/*===================================================================================
+ *  COVARIATE CLASSIFICATION PREFERENCE PROCESSING FUNCTIONS
+ *  
+ *  The following functions are utilized to present heat map covariate classfication
+ *  bar configuration options:
+ *  	- setupClassPrefs
+ *  	- setupClassBreaks
+ *  	- setupAllClassesPrefs
+ *      - showAllBars
+ *      - setShowAll
+ =================================================================================*/
+
+/**********************************************************************************
+ * FUNCTION - setupClassPrefs: The purpose of this function is to construct a DIV 
+ * panel containing all covariate bar preferences.  A dropdown list containing all 
+ * covariate classification bars is presented and individual DIVs for each data layer, 
+ * containing  breakpoints/colors, are added. Additionally, a "front panel" DIV is
+ * created for "ALL" classification bars that contains preferences that are global
+ * to all of the individual bars.
+ **********************************************************************************/
+function setupClassPrefs(e, prefprefs){
+	var classBars = heatMap.getClassifications();
+	var classprefs = getDivElement("classPrefs");
+	var prefContents = document.createElement("TABLE");
+	prefContents.insertRow().innerHTML = formatBlankRow();
+	var filterInput = "<input name='all_searchPref' id='all_searchPref'>";
+	var filterButton = "<img id='all_searchPref_btn' src='" + staticPath + "images/filterClassButton.png' alt='Search Covariates' onclick='filterClassPrefs(true);' align='top'/>";
+	if (filterVal != null) {
+		var filterInput = "<input name='all_searchPref' id='all_searchPref' value='"+filterVal+"'>";
+		var filterButton = "<img id='all_searchPref_btn' src='" + staticPath + "images/removeFilterClassButton.png' alt='Search Covariates' onclick='filterClassPrefs(false);' align='top'/>";
+	}
+	var searchClasses = filterInput+"&nbsp;&nbsp;"+filterButton;
+	setTableRow(prefContents,[searchClasses], 4, 'right');
+	prefContents.insertRow().innerHTML = formatBlankRow();
+	prefContents.insertRow().innerHTML = formatBlankRow();
+	var classSelect = "<select name='classPref_list' id='classPref_list' onchange='showClassBreak();'>"
+    classSelect = classSelect+"<option value='ALL'>ALL</option>";
+	for (var key in classBars){
+		if (filterShow(key)) {
+			classSelect = classSelect+"<option value='"+classBars[key].colorScheme+"'>"+key+"</option>";
+		}
+	}
+	classSelect = classSelect+"</select>"
+	setTableRow(prefContents,["Covariate Bar: ", classSelect]);
+	prefContents.insertRow().innerHTML = formatBlankRow();
+	classprefs.appendChild(prefContents);
+	var i = 0;
+	for (var key in classBars){
+		if (filterShow(key)) {
+			var breakprefs = setupClassBreaks(e, classBars[key].colorScheme, key);
+			breakprefs.style.display="none";
+			breakprefs.style.width = 300;
+			classprefs.appendChild(breakprefs);
+		}
+		i++;
+	}
+	// Append a DIV panel for all of the covariate class bars 
+	var allPrefs = setupAllClassesPrefs(e); 
+	allPrefs.style.display="block";
+	classprefs.appendChild(allPrefs);
+	return classprefs;
+}
+
+/**********************************************************************************
+ * FUNCTION - setupClassBreaks: The purpose of this function is to construct a DIV 
+ * containing a list of all covariate bars with informational data and user preferences 
+ * that are common to all bars (show/hide and size).  
+ **********************************************************************************/
+function setupAllClassesPrefs(e){
+	var allprefs = getDivElement("breakPrefs_ALL");
+	var prefContents = document.createElement("TABLE");
+	var rowCtr = 0;
+	prefContents.insertRow().innerHTML = formatBlankRow();  
+	var colShowAll = "<input name='all_showPref' id='all_showPref' type='checkbox' onchange='showAllBars();'> ";
+	setTableRow(prefContents,["<u>"+"Classification"+"</u>", "<b><u>"+"Position"+"</u></b>", colShowAll+"<b><u>"+"Show"+"</u></b>", "<b><u>"+"Height"+"</u></b>"]);
+	rowCtr=2;
+	var classBars = heatMap.getClassifications();
+	var checkState = true;
+	for (var key in classBars){
+		if (filterShow(key)) {
+			var colShow = "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<input name='"+key+"_showPref' id='"+key+"_showPref' type='checkbox' onchange='setShowAll();'";
+			if (classBars[key].show == 'Y') {
+				colShow = colShow+"checked"
+			}
+			colShow = colShow+ " >";
+			var colHeight = "<input name='"+key+"_heightPref' id='"+key+"_heightPref' value='"+classBars[key].height+"' maxlength='2' size='2'>";
+			setTableRow(prefContents,[key,toTitleCase(classBars[key].position),colShow,colHeight]); 
+			rowCtr++;
+		}
+	}
+	allprefs.appendChild(prefContents);
+	if (rowCtr > maxRows) {
+		maxRows = rowCtr;
+	}
+	return allprefs;
+}	
+
+/**********************************************************************************
+ * FUNCTION - setupClassBreaks: The purpose of this function is to construct a DIV 
+ * containing a set informational data and a list of categories/colors for a given
+ * covariate classfication bar.  
+ **********************************************************************************/
+function setupClassBreaks(e, mapName, barName){
+	var classBars = heatMap.getClassifications();
+	var colorMap = heatMap.getColorMapManager().getColorMap(mapName);
+	var thresholds = colorMap.getThresholds();
+	var colors = colorMap.getColors();
+	var helpprefs = getDivElement("breakPrefs_"+mapName);
+	var prefContents = document.createElement("TABLE"); 
+	var rowCtr = 0;
+	prefContents.insertRow().innerHTML = formatBlankRow();
+	var colShow = "<input name='"+barName+"_showPref' id='"+barName+"_showPref' type='checkbox' ";
+	if (classBars[barName].show == 'Y') {
+		colShow = colShow+"checked"
+	}
+	colShow = colShow+ " >";
+	var colHeight = "<input name='"+barName+"_heightPref' id='"+barName+"_heightPref' value='"+classBars[barName].height+"' maxlength='2' size='2'>";
+	var pos = toTitleCase(classBars[barName].position);
+	var typ = toTitleCase(colorMap.getType());
+	if (classBars[barName].position == "row") {
+		pos = "Row";
+	}
+	setTableRow(prefContents,["Bar Position: ","<b>"+pos+"</b>"]);
+	setTableRow(prefContents,["Bar Type: ","<b>"+typ+"</b>"]);
+	prefContents.insertRow().innerHTML = formatBlankRow();
+	prefContents.insertRow().innerHTML = formatBlankRow();
+	rowCtr = rowCtr+4;
+	prefContents.insertRow().innerHTML = formatBlankRow();
+	rowCtr++;
+	setTableRow(prefContents, ["<u>Category</u>", "<b><u>"+"Color"+"</u></b>"]); 
+	rowCtr++;
+	for (var j = 0; j < thresholds.length; j++) {
+		var threshold = thresholds[j];
+		var color = colors[j];
+		var threshId = mapName+"_breakPt"+j;
+		var colorId = mapName+"_color"+j;
+		var colorInput = "<input class='spectrumColor' type='color' name='"+colorId+"_colorPref' id='"+colorId+"_colorPref' value='"+color+"'>"; 
+		setTableRow(prefContents, [threshold, colorInput]);
+		rowCtr++;
+	} 
+	prefContents.insertRow().innerHTML = formatBlankRow();
+	rowCtr++;
+	setTableRow(prefContents, ["Missing Color:",  "<input class='spectrumColor' type='color' name='"+mapName+"_missing_colorPref' id='"+mapName+"_missing_colorPref' value='"+colorMap.getMissingColor()+"'>"]);
+	rowCtr++;
+	if (rowCtr > maxRows) {
+		maxRows = rowCtr;
+	}
+	helpprefs.style.height = rowCtr;
+	helpprefs.style.width = 30;
+	helpprefs.appendChild(prefContents);
+	return helpprefs;
+}	
+
+/**********************************************************************************
+ * FUNCTION - showAllBars: The purpose of this function is to set the condition of
+ * the "show" checkbox for all covariate bars on the covariate bars tab of the user 
+ * preferences dialog. These checkboxes are located on the DIV that is visible when 
+ * the ALL entry of the covariate dropdown is selected. Whenever a  user checks the 
+ * show all box, all other boxes are checked.  
+ **********************************************************************************/
+function showAllBars(){
+	var classBars = heatMap.getClassifications();
+	var showAllBox = document.getElementById('all_showPref');
+	var checkState = false;
+	if (showAllBox.checked) {
+		checkState = true;
+	}
+	for (var key in classBars){
+		if (filterShow(key)) {
+			var colShow = document.getElementById(key+'_showPref');
+			colShow.checked = checkState;
+		}
+	}
+	return;
+}	
+
+/**********************************************************************************
+ * FUNCTION - setShowAll: The purpose of this function is to set the condition of
+ * the "show all" checkbox on the covariate bars tab of the user preferences dialog.
+ * This checkbox is located on the DIV that is visible when the ALL entry of the 
+ * covariate dropdown is selected. If a user un-checks a single box in the list of 
+ * covariate bars, the show all box is un-checked. Conversely, if a user checks a box 
+ * resulting in all of the boxes being selected, the show all box will be checked.
+ **********************************************************************************/
+function setShowAll(){
+	var classBars = heatMap.getClassifications();
+	var checkState = true;
+	for (var key in classBars){
+		var colShow = document.getElementById(key+'_showPref');
+		if (filterShow(key)) {
+			if (!colShow.checked) {
+				checkState = false;
+				break;
+			}
+		}
+	}
+	var showAllBox = document.getElementById('all_showPref');
+	showAllBox.checked = checkState;
+	return;
+}	
+
+
+/**********************************************************************************
+ * FUNCTION - showClassBreak: The purpose of this function is to show the 
+ * appropriate classification bar panel based upon the user selection of the 
+ * covariate dropdown on the covariates tab of the preferences screen.  This 
+ * function is also called when an error is trappped, opening the covariate DIV
+ * that contains the erroneous data entry.
+ **********************************************************************************/
+function showClassBreak(selClass) {
+	var classBtn = document.getElementById("classPref_list");
+	if (typeof selClass != 'undefined') {
+		classBtn.value = selClass;
+	} 
+	for (var i=0; i<classBtn.length; i++){
+		var classVal = "breakPrefs_"+classBtn.options[i].value;
+		var classDiv = document.getElementById(classVal);
+		var classSel = classBtn.options[i].selected;
+		if (classSel) {
+			classDiv.style.display = "block";
+		} else {
+			classDiv.style.display = "none";
+		}
+	}
+}
+
+/**********************************************************************************
+ * FUNCTION - filterClassPrefs: The purpose of this function is to initiate the 
+ * process of filtering option choices for classifications. It is fired when either
+ * the "Filter Covariates" or "Clear Filters" button is pressed on the covariates 
+ * preferences dialog.  The global filter value variable is set when filtering and 
+ * cleared when clearing and the editPreferences function is called to reload all
+ * preferences.
+ **********************************************************************************/
+function filterClassPrefs(filterOn){
+	searchPerformed = true;
+	if (filterOn) {
+		var searchPrefSelect = document.getElementById('all_searchPref');
+		var searchPrefVal = searchPrefSelect.value;
+		if (searchPrefVal != "") {
+			filterVal = searchPrefVal;
+		} else {
+			filterVal = null;
+		}
+	} else {
+		filterVal = null;
+	}
+	var prefspanel = document.getElementById('prefsPanel');
+	if (prefspanel){
+		prefspanel.remove();
+	}
+	editPreferences(document.getElementById('gear_btn'));
+}
+
+/**********************************************************************************
+ * FUNCTION - filterShow: The purpose of this function is to determine whether a 
+ * given covariates bar is to be shown given the state of the covariates filter
+ * search text box.
+ **********************************************************************************/
+function filterShow(key) {
+	var filterShow = false;
+	var lowerkey = key.toLowerCase();
+	if (filterVal != null) {
+		if (lowerkey.indexOf(filterVal.toLowerCase()) >= 0) {
+			filterShow = true;
+		}
+	} else {
+		filterShow = true;
+	}
+	return filterShow;
+	
+}
+
+/*===================================================================================
+ *  ROW COLUMN PREFERENCE PROCESSING FUNCTIONS
+ *  
+ *  The following functions are utilized to present heat map covariate classfication
+ *  bar configuration options:
+ *  	- setupRowColPrefs
+ *  	- showDendroSelections
+ *      - dendroRowShowChange
+ *      - dendroColShowChange
+ =================================================================================*/
+
+/**********************************************************************************
+ * FUNCTION - setupRowColPrefs: The purpose of this function is to construct a DIV 
+ * panel containing all row & col preferences.  Two sections are presented, one for
+ * rows and the other for cols.  Informational data begins each section and 
+ * properties for modifying the appearance of row/col dendograms appear at the end.
+ **********************************************************************************/
+function setupRowColPrefs(e, prefprefs){
+	var rowcolprefs = getDivElement("rowsColsPrefs");
+	var prefContents = document.createElement("TABLE");
+	prefContents.insertRow().innerHTML = formatBlankRow();
+	setTableRow(prefContents,["ROW INFORMATION:"], 2);
+	prefContents.insertRow().innerHTML = formatBlankRow();
+	var rowLabels = heatMap.getRowLabels();
+	var dendrogram = heatMap.getDendrogram();
+	var rowOrder = rowLabels['order_method'];
+	setTableRow(prefContents,["&nbsp;&nbsp;Labels Type:",rowLabels['label_type']]);
+	setTableRow(prefContents,["&nbsp;&nbsp;Ordering Method:",rowOrder]);
+	var rowCtr = 5;
+	var dendroShowOptions = "<option value='ALL'>Summary and Detail</option><option value='SUMMARY'>Summary Only</option><option value='NONE'>Hide</option></select>";
+	var dendroHeightOptions = "<option value='50'>50%</option><option value='75'>75%</option><option value='100'>100%</option><option value='125'>125%</option><option value='150'>150%</option><option value='200'>200%</option><option value='300'>300%</option></select>";
+	if (rowOrder === "Hierarchical") {
+		setTableRow(prefContents,["&nbsp;&nbsp;Agglomeration Method:",rowLabels['agglomeration_method']]);
+		rowCtr++;
+		setTableRow(prefContents,["&nbsp;&nbsp;Distance Metric:",rowLabels['distance_metric']]);
+		rowCtr++;
+		var rowDendroSelect = "<select name='rowDendroShowPref' id='rowDendroShowPref' onchange='dendroRowShowChange()'>"
+		rowDendroSelect = rowDendroSelect+dendroShowOptions;
+		setTableRow(prefContents,["&nbsp;&nbsp;Show Dendrogram:",rowDendroSelect]);  
+		rowCtr++;
+		var rowDendroHeightSelect = "<select name='rowDendroHeightPref' id='rowDendroHeightPref'>"
+		rowDendroHeightSelect = rowDendroHeightSelect+dendroHeightOptions;
+		setTableRow(prefContents,["&nbsp;&nbsp;Dendrogram Height:",rowDendroHeightSelect]); 
+		rowCtr++;
+	}  
+	prefContents.insertRow().innerHTML = formatBlankRow(); 
+	rowCtr++;
+	prefContents.insertRow().innerHTML = formatBlankRow();
+	rowCtr++;
+	setTableRow(prefContents,["COLUMN INFORMATION:"], 2);
+	rowCtr++;
+	prefContents.insertRow().innerHTML = formatBlankRow();
+	
+	var colLabels = heatMap.getColLabels();
+	var colOrder = colLabels['order_method'];
+	setTableRow(prefContents,["&nbsp;&nbsp;Labels Type:",colLabels['label_type']]);
+	rowCtr++;
+	setTableRow(prefContents,["&nbsp;&nbsp;Ordering Method:",colOrder]);
+	rowCtr++;
+	if (colOrder === "Hierarchical") {
+		setTableRow(prefContents,["&nbsp;&nbsp;Agglomeration Method:",colLabels['agglomeration_method']]);
+		rowCtr++;
+		setTableRow(prefContents,["&nbsp;&nbsp;Distance Metric:",colLabels['distance_metric']]);
+		rowCtr++;
+		var colDendroShowSelect = "<select name='colDendroShowPref' id='colDendroShowPref' onchange='dendroColShowChange()'>"
+		colDendroShowSelect = colDendroShowSelect+dendroShowOptions;
+		var colDendroHeightSelect = "<select name='colDendroHeightPref' id='colDendroHeightPref'>"
+		colDendroHeightSelect = colDendroHeightSelect+dendroHeightOptions;
+		setTableRow(prefContents,["&nbsp;&nbsp;Show Dendrogram:",colDendroShowSelect]);
+		rowCtr++;
+		setTableRow(prefContents,["&nbsp;&nbsp;Dendrogram Height:",colDendroHeightSelect]);
+		rowCtr++;
+	}
+	if (rowCtr > maxRows) {
+		maxRows = rowCtr;
+	}
+	rowcolprefs.appendChild(prefContents);
+	return rowcolprefs;
+}
+
+/**********************************************************************************
+ * FUNCTION - showDendroSelections: The purpose of this function is to set the 
+ * states of the row/column dendrogram show and height preferences.
+ **********************************************************************************/
+function showDendroSelections() {
+	var dendrogram = heatMap.getDendrogram();
+	var rowLabels = heatMap.getRowLabels();
+	var rowOrder = rowLabels['order_method'];
+	if (rowOrder === "Hierarchical") {
+		var dendroShowVal = dendrogram['row_dendro_show'];
+		document.getElementById("rowDendroShowPref").value = dendroShowVal;
+		var rowHeightPref = document.getElementById("rowDendroHeightPref");
+		if (dendroShowVal === 'NONE') {
+			var opt = rowHeightPref.options[6];
+			if (typeof opt != 'undefined') {
+				rowHeightPref.options[6].remove();
+			}
+			var option = document.createElement("option");
+			option.text = "NA";
+			option.value = '10';
+			rowHeightPref.add(option);
+			rowHeightPref.disabled = true;
+		}
+		rowHeightPref.value = dendrogram['row_dendro_height'];
+	}
+	var colLabels = heatMap.getColLabels();
+	var colOrder = colLabels['order_method'];
+	if (colOrder === "Hierarchical") {
+		var dendroShowVal = dendrogram['col_dendro_show'];
+		document.getElementById("colDendroShowPref").value = dendroShowVal;
+		var colHeightPref = document.getElementById("colDendroHeightPref");
+		if (dendroShowVal === 'NONE') {
+			var opt = colHeightPref.options[6];
+			if (typeof opt != 'undefined') {
+				colHeightPref.options[6].remove();
+			}
+			var option = document.createElement("option");
+			option.text = "NA";
+			option.value = '10';
+			colHeightPref.add(option);
+			colHeightPref.disabled = true;
+		}
+		colHeightPref.value = dendrogram['col_dendro_height'];
+	}
+}
+
+/**********************************************************************************
+ * FUNCTION - dendroRowShowChange: The purpose of this function is to respond to
+ * a change event on the show row dendrogram dropdown.  If the change is to Hide, 
+ * the row dendro height is set to 10 and the dropdown disabled. If the change is to
+ * one of the 2 Show options AND was previously Hide, set height to the default
+ * value of 100 and enable the dropdown.
+ **********************************************************************************/
+function dendroRowShowChange() {
+	var newValue = document.getElementById("rowDendroShowPref").value;
+	var rowHeightPref = document.getElementById("rowDendroHeightPref");
+	if (newValue === 'NONE') {
+		var option = document.createElement("option");
+		option.text = "NA";
+		option.value = '10';
+		rowHeightPref.add(option);
+		rowHeightPref.value = '10';
+		rowHeightPref.disabled = true;
+	} else if (rowHeightPref.disabled) {
+		var opt = rowHeightPref.options[6];
+		if (typeof opt != 'undefined') {
+			rowHeightPref.options[6].remove();
+		}
+		rowHeightPref.value = '100';
+		rowHeightPref.disabled = false;
+	}
+}
+
+/**********************************************************************************
+ * FUNCTION - dendroRowShowChange: The purpose of this function is to respond to
+ * a change event on the show row dendrogram dropdown.  If the change is to Hide, 
+ * the row dendro height is set to 10 and the dropdown disabled. If the change is to
+ * one of the 2 Show options AND was previously Hide, set height to the default
+ * value of 100 and enable the dropdown.
+ **********************************************************************************/
+function dendroColShowChange() {
+	var newValue = document.getElementById("colDendroShowPref").value;
+	var colHeightPref = document.getElementById("colDendroHeightPref");
+	if (newValue === 'NONE') {
+		var option = document.createElement("option");
+		option.text = "NA";
+		option.value = '10';
+		colHeightPref.add(option);
+		colHeightPref.value = '10';
+		colHeightPref.disabled = true;
+	} else if (colHeightPref.disabled) {
+		var opt = colHeightPref.options[6];
+		if (typeof opt != 'undefined') {
+			colHeightPref.options[6].remove();
+		}
+		colHeightPref.value = '100';
+		colHeightPref.disabled = false;
+	}
+}
+
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mda_heatmap_viz/static/javascript/UserPreferenceManager.js.sav	Thu Apr 07 14:43:37 2016 -0400
@@ -0,0 +1,1159 @@
+/**********************************************************************************
+ * USER PREFERENCE FUNCTIONS:  The following functions handle the processing 
+ * for user preference editing. 
+ **********************************************************************************/
+
+//Global variables for preference processing
+var maxRows = 0;
+var helpRowSize = 0;
+var bkpColorMap = null;
+var filterVal;
+var searchPerformed = false;
+
+/*===================================================================================
+ *  COMMON PREFERENCE PROCESSING FUNCTIONS
+ *  
+ *  The following functions are utilized to present the entire heat map preferences
+ *  dialog and, therefore, sit above those functions designed specifically for processing
+ *  individual data layer and covariate classification bar preferences:
+ *  	- editPreferences
+ *  	- setPrefsDivSizing
+ *  	- showLayerPrefs
+ *      - showClassPrefs
+ *      - showRowsColsPrefs
+ *      - prefsCancel
+ *      - prefsApply
+ *      - prefsValidate
+ *      - prefsValidateBreakPoints
+ *      - prefsValidateBreakColors
+ *      - prefsApplyBreaks
+ *      - getNewBreakColors
+ *      - getNewBreakThresholds  
+ *      - prefsSave
+ =================================================================================*/
+
+/**********************************************************************************
+ * FUNCTION - editPreferences: This is the MAIN driver function for edit 
+ * preferences processing.  It is called under two conditions (1) The Edit 
+ * preferences "gear" button is pressed on the main application screen 
+ * (2) User preferences have been applied BUT errors have occurred.
+ * 
+ * Processing for this function is documented in detail in the body of the function.
+ **********************************************************************************/
+function editPreferences(e,errorMsg){
+	maxRows = 0;
+	userHelpClose();
+
+	// If helpPrefs element already exists, the user is pressing the gear button
+	// when preferences are already open. Disregard.
+	var helpExists = document.getElementById('prefsPanel');
+	if ((isSub) || (helpExists !== null)) {
+		return;
+	}
+
+	//If first time thru, save the dataLayer colorMap
+	//This is done because the colorMap must be edited to add/delete breakpoints while retaining their state
+	var colorMap = heatMap.getColorMapManager().getColorMap("dl1"); //TODO - Modify when multiple data layers (flick) are added
+	if (bkpColorMap === null) {
+		bkpColorMap = colorMap;
+	} 
+
+	//Create a "master" DIV for displaying edit preferences
+	var prefspanel = getDivElement("prefsPanel");
+	document.getElementsByTagName('body')[0].appendChild(prefspanel);
+	
+	//Create a one cell table and populate the header and tab elements in the first 3 table rows
+	var prefContents = document.createElement("TABLE");
+	var headDiv = document.createElement('div');
+	headDiv.className = 'prefsHeader';
+	headDiv.id = 'prefsHeader';
+	prefspanel.appendChild(headDiv);
+	headDiv.textContent = 'Heat Map Display Properties';
+	
+	prefContents.insertRow().innerHTML = formatBlankRow();
+	prefContents.insertRow().innerHTML = "<td style='line-height:30px;'>&nbsp;</td>";
+	prefContents.insertRow().innerHTML = "<td style='border-bottom-style:solid;border-bottom-width:2px;position: relative;'><div id='prefTab_buttons' style='position: absolute; bottom: 0;' align='left'><img id='prefRowsCols_btn' src='/plugins/visualizations/mda_heatmap_viz/static/images/rowsColsOn.png' alt='Edit Rows & Columns' onclick='showRowsColsPrefs();' align='top'/>&nbsp;<img id='prefLayer_btn' src='/plugins/visualizations/mda_heatmap_viz/static/images/dataLayersOff.png' alt='Edit Data Layers' onclick='showLayerPrefs();' align='top'/>&nbsp;<img id='prefClass_btn' src='/plugins/visualizations/mda_heatmap_viz/static/images/covariateBarsOff.png' alt='Edit Classifications' onclick='showClassPrefs();' align='top'/></div></td>";
+	//Initialize rowCtr variable
+	var rowCtr = 3;
+
+	//Create a parent DIV as a container for breakpoint and classification edit display
+	var prefprefs = getDivElement("prefprefs"); 
+
+	//Create and populate row & col preferences DIV and add to parent DIV
+	var rowcolprefs = setupRowColPrefs(e, prefprefs);
+	rowcolprefs.style.display="none";
+	prefprefs.appendChild(rowcolprefs);
+
+	//Create and populate classifications preferences DIV and add to parent DIV
+	var classprefs = setupClassPrefs(e, prefprefs);
+	classprefs.style.display="none";
+	prefprefs.appendChild(classprefs);
+	
+	//Create and populate breakpoint preferences DIV and add to parent DIV
+	var layerprefs = setupLayerPrefs(e, prefprefs);
+	layerprefs.style.display="none";
+	prefprefs.appendChild(layerprefs);
+
+	// Set DIV containing both class and break DIVs to visible and append to prefspanel table
+	prefprefs.style.display="block";
+	var row1 = prefContents.insertRow();
+	var row1Cell = row1.insertCell(0);
+	row1Cell.appendChild(prefprefs);
+	
+	//If error message exists add table row to prefspanel table containing error message
+	if (errorMsg != null) {
+		setErrorRow(prefContents, errorMsg[2]);
+	}
+
+	var prefButtons = document.createElement("TABLE");
+	//Add Cancel, Apply, and Save buttons to bottom of prefspanel table
+	var buttons = "<img id='prefCancel_btn' src='/plugins/visualizations/mda_heatmap_viz/static/images/prefCancel.png' alt='Cancel changes' onclick='prefsCancelButton();' align='top'/>&nbsp;<img id='prefApply_btn' src='/plugins/visualizations/mda_heatmap_viz/static/images/prefApply.png' alt='Apply changes' onclick='prefsApplyButton();' align='top'/>";
+	if (heatMap.isSaveAllowed()) {
+		buttons = buttons + "&nbsp;<img id='prefSave_btn' src='/plugins/visualizations/mda_heatmap_viz/static/images/prefSave.png' alt='Save changes' onclick='prefsSaveButton();' align='top'/>";
+	}
+	setTableRow(prefButtons,["<div id='pref_buttons' align='right'>"+buttons+"</div>"]);
+	rowCtr++;
+	prefprefs.appendChild(prefButtons);
+
+	//Add prefspanel table to the main preferences DIV and set position and display
+	prefspanel.appendChild(prefContents);
+    prefspanel.style.position = "absolute";
+	prefspanel.style.top = e.offsetTop + 15;
+	prefspanel.style.display="inherit";
+
+	//maxRows has been loaded with a count of the datalayer/class panel with the most rows
+	//add to this the number of rows added during construction of prefspanel table.
+	maxRows = maxRows+rowCtr;
+	//Retrieve maximum row height size used in various preferences DIVs
+	helpRowSize = parseFloat(getStyle(prefspanel, 'font-size' ), 10)*1.45;
+	//Use the two above numbers to apply sizing to all of the preferences DIVs.
+	setPrefsDivSizing();
+	prefspanel.style.left = e.offsetLeft - parseInt(layerprefs.style.width,10);
+	
+	//If errors exist and they are NOT on the currently visible DIV (dataLayer1),
+	//hide the dataLayers DIV, set the tab to "Covariates", and open the appropriate
+	//covariate bar DIV.
+	showDendroSelections();
+	setShowAll();
+	if ((errorMsg != null) && (errorMsg[1] === "classPrefs")) {
+		showClassBreak(errorMsg[0]);
+		showClassPrefs();
+	} else if ((errorMsg != null) && (errorMsg[1] === "layerPrefs")){ 
+		showLayerPrefs();
+	} else if (searchPerformed){ 
+		searchPerformed = false;
+		showClassPrefs();
+	} else {
+		showRowsColsPrefs();
+	}
+
+}
+
+/**********************************************************************************
+ * FUNCTION - setPrefsDivSizing: The purpose of this function is to resize the
+ * various DIVs inside the preferences dialog.  It is called when the dialog is 
+ * first opened and whenever data layer breakpoints are added (if necessary).
+ **********************************************************************************/
+//TODO - This can be improved as the current sizing is not exact.
+function setPrefsDivSizing() {
+	var rowsColsprefs = document.getElementById("rowsColsPrefs");
+	var layerprefs = document.getElementById("layerPrefs");
+	var classprefs = document.getElementById("classPrefs");
+	var helprefs = document.getElementById("prefsPanel");
+	var prefHeight = maxRows*helpRowSize;
+	var prefWidth = 380;
+	rowsColsprefs.style.width = prefWidth;
+	rowsColsprefs.style.height = prefHeight;
+	layerprefs.style.width = prefWidth;
+	layerprefs.style.height = prefHeight;
+	classprefs.style.width = prefWidth;
+	classprefs.style.height = prefHeight;
+}
+
+/**********************************************************************************
+ * FUNCTION - showLayerPrefs: The purpose of this function is to perform the 
+ * processing for the preferences tab when the user selects the "Data Layers" tab.
+ **********************************************************************************/
+function showRowsColsPrefs() {
+	//Turn off all tabs
+	hideAllPrefs();
+	//Turn on layer prefs tab
+	var rowsColsBtn = document.getElementById("prefRowsCols_btn");
+	rowsColsBtn.setAttribute('src', '/plugins/visualizations/mda_heatmap_viz/static/images/rowsColsOn.png');
+	var rowsColsDiv = document.getElementById("rowsColsPrefs");
+	rowsColsDiv.style.display="block";
+}
+
+
+/**********************************************************************************
+ * FUNCTION - showLayerPrefs: The purpose of this function is to perform the 
+ * processing for the preferences tab when the user selects the "Data Layers" tab.
+ **********************************************************************************/
+function showLayerPrefs() {
+	//Turn off all tabs
+	hideAllPrefs();
+	//Turn on layer prefs tab
+	var layerBtn = document.getElementById("prefLayer_btn");
+	layerBtn.setAttribute('src', '/plugins/visualizations/mda_heatmap_viz/static/images/dataLayersOn.png');
+	var layerDiv = document.getElementById("layerPrefs");
+	layerDiv.style.display="block";
+}
+
+/**********************************************************************************
+ * FUNCTION - showClassPrefs: The purpose of this function is to perform the 
+ * processing for the preferences tab when the user selects the "Covariates" tab.
+ **********************************************************************************/
+function showClassPrefs() {
+	//Turn off all tabs
+	hideAllPrefs();
+	//Turn on classification prefs tab
+	var classBtn = document.getElementById("prefClass_btn");
+	classBtn.setAttribute('src', '/plugins/visualizations/mda_heatmap_viz/static/images/covariateBarsOn.png');
+	var classDiv = document.getElementById("classPrefs");
+	classDiv.style.display="block";
+}
+
+/**********************************************************************************
+ * FUNCTION - hideAllPrefs: The purpose of this function is to set all tabs off. It 
+ * is called whenever a tab is clicked.  All tabs are set to hidden with their
+ * image set to the "off" image.
+ **********************************************************************************/
+function hideAllPrefs() {
+	var classBtn = document.getElementById("prefClass_btn");
+	classBtn.setAttribute('src', '/plugins/visualizations/mda_heatmap_viz/static/images/covariateBarsOff.png');
+	var classDiv = document.getElementById("classPrefs");
+	classDiv.style.display="none";
+	var layerBtn = document.getElementById("prefLayer_btn");
+	layerBtn.setAttribute('src', '/plugins/visualizations/mda_heatmap_viz/static/images/dataLayersOff.png');
+	var layerDiv = document.getElementById("layerPrefs");
+	layerDiv.style.display="none";
+	var rowsColsBtn = document.getElementById("prefRowsCols_btn");
+	rowsColsBtn.setAttribute('src', '/plugins/visualizations/mda_heatmap_viz/static/images/rowsColsOff.png');
+	var rowsColsDiv = document.getElementById("rowsColsPrefs");
+	rowsColsDiv.style.display="none";
+}
+
+/**********************************************************************************
+ * FUNCTION - prefsCancelButton: The purpose of this function is to perform all processing
+ * necessary to exit the user preferences dialog WITHOUT applying or saving any 
+ * changes made by the user when the Cancel button is pressed on the ColorMap 
+ * preferences dialog.  Since the dataLayer colormap must be edited to add/delete
+ * breakpoints, the backup colormap (saved when preferences are first opened) is re-
+ * applied to the colorMapManager.  Then the preferences DIV is retrieved and removed.
+ **********************************************************************************/
+function prefsCancelButton() {
+	if (bkpColorMap !== null) {
+		var colorMapMgr = heatMap.getColorMapManager();
+		colorMapMgr.setColorMap("dl1", bkpColorMap);
+	}
+	var prefspanel = document.getElementById('prefsPanel');
+	if (prefspanel){
+		prefspanel.remove();
+	}
+}
+
+/**********************************************************************************
+ * FUNCTION - prefsApplyButton: The purpose of this function is to perform all processing
+ * necessary to reconfigure the "current" presentation of the heat map in the 
+ * viewer when the Apply button is pressed on the ColorMap Preferences Dialog.  
+ * First validations are performed.  If errors are found, preference 
+ * changes are NOT applied and the user is re-presented with the preferences dialog
+ * and the error found.  If no errors are found, all changes are applied to the heatmap 
+ * and the summary panel, detail panel, and covariate bars are redrawn.  However, 
+ * these changes are not yet permanently  saved to the JSON files that are used to 
+ * configure heat map presentation.
+ **********************************************************************************/
+function prefsApplyButton() {
+	//Perform validations of all user-entered data layer and covariate bar
+	//preference changes.
+	var errorMsg = prefsValidate();
+	prefsApply();
+	if (errorMsg !== null) {
+		prefsError(errorMsg);
+	} else { 
+		prefsSuccess();
+	}
+}
+
+/**********************************************************************************
+ * FUNCTION - prefsSaveButton: The purpose of this function is to perform all processing
+ * necessary to permanently save user preference changes.  This will result in 
+ * changes to the JSON files that are used to configure heat map presentation.
+ **********************************************************************************/
+function prefsSaveButton() {
+	var mode = heatMap.mode;
+	var errorMsg = prefsValidate();
+	prefsApply();
+	if (errorMsg !== null) {
+		prefsError(errorMsg);
+	} else { 
+		var success = heatMap.saveHeatMapProperties();
+		if (success === "false") {
+			prefsError(["dl1", "layerPrefs", "ERROR: Preferences failed to save. Use Apply or Cancel to continue."]);
+		} else {
+			prefsSuccess();
+		}
+	}
+}
+
+/**********************************************************************************
+ * FUNCTION - prefsSuccess: The purpose of this function perform the functions
+ * necessary when preferences are determined to be valid. It is shared by the
+ * Apply and Save buttons.
+ **********************************************************************************/
+function prefsSuccess() {
+	filterVal = null;
+	//Remove the backup color map (used to reinstate colors if user cancels)
+	//and formally apply all changes to the heat map, re-draw, and exit preferences.
+	bkpColorMap = null;
+	summaryInit();
+	detailInit();
+	changeMode('NORMAL');
+	prefsCancelButton();
+}
+
+/**********************************************************************************
+ * FUNCTION - prefsError: The purpose of this function perform the functions
+ * necessary when preferences are determined to be invalid. It is shared by the
+ * Apply and Save buttons.
+ **********************************************************************************/
+function prefsError(errorMsg) {
+	//If a validation error exists, re-present the user preferences
+	//dialog with the error message displayed in red. 
+	var prefspanel = document.getElementById('prefsPanel');
+	if (prefspanel){
+		prefspanel.remove();
+	}
+	editPreferences(document.getElementById('gear_btn'),errorMsg);
+}
+
+/**********************************************************************************
+ * FUNCTION - prefsSuccess: The purpose of this function is to apply all user
+ * ColorMap preferences settings.  It is shared by the Apply and Save buttons.
+ **********************************************************************************/
+function prefsApply() {
+	// Apply Row & Column Preferences
+	var dendrogram = heatMap.getDendrogram();
+	var rowLabels = heatMap.getRowLabels();
+	var rowOrder = rowLabels['order_method'];
+	if (rowOrder === "Hierarchical") {
+		var rowDendroShowVal = document.getElementById("rowDendroShowPref").value;
+		dendrogram['row_dendro_show'] = rowDendroShowVal;
+		dendrogram['row_dendro_height'] = document.getElementById("rowDendroHeightPref").value;
+	}	
+	var colLabels = heatMap.getColLabels();
+	var colOrder = colLabels['order_method'];
+	if (colOrder === "Hierarchical") {
+		var colDendroShowVal = document.getElementById("colDendroShowPref").value;
+		dendrogram['col_dendro_show'] = colDendroShowVal;
+		dendrogram['col_dendro_height'] = document.getElementById("colDendroHeightPref").value;
+	}	
+	// Apply Covariate Bar Preferences
+	var classBars = heatMap.getClassifications();
+	for (var key in classBars){
+		var showElement = document.getElementById(key+"_showPref");
+		var heightElement = document.getElementById(key+"_heightPref");
+		if (filterShow(key)) {
+			heatMap.setClassificationPrefs(key,showElement.checked,heightElement.value);
+		} else {
+			heatMap.setClassificationPrefs(key,false,15);
+		}
+		prefsApplyBreaks(classBars[key].colorScheme,"covariate",filterShow(key));
+	}
+	// Apply Data Layer Preferences
+	//TODO - Future loop for data layers
+	prefsApplyBreaks("dl1","datalayer",true);
+}
+
+/**********************************************************************************
+ * FUNCTION - prefsValidate: The purpose of this function is to validate all user
+ * changes to the heatmap properties. When the very first error is found, an error 
+ * message (string array containing error information) is created and returned to 
+ * the prefsApply function. 
+ **********************************************************************************/
+function prefsValidate() {
+	var classBars = heatMap.getClassifications();
+	var errorMsg = null;
+	//Loop thru all covariate classfication bars validating all break colors
+	for (var key in classBars){
+		if (filterShow(key)) {
+			var showElement = document.getElementById(key+"_showPref");
+			var heightElement = document.getElementById(key+"_heightPref");
+			errorMsg = prefsValidateBreakColors(classBars[key].colorScheme,"classPrefs");
+			if (errorMsg !== null) break;
+		}
+	}
+	//Validate all breakpoints and colors for the main data layer
+	if (errorMsg === null) {
+		//TODO: currently only processing for data layer 1. This will require modification
+		// when new data layers (e.g. flicks) are added to the heatmap.
+		errorMsg = prefsValidateBreakPoints("dl1","layerPrefs");
+		if (errorMsg === null) {
+			errorMsg = prefsValidateBreakColors("dl1","layerPrefs");
+		}
+	}
+	return errorMsg;
+}
+
+/**********************************************************************************
+ * FUNCTION - prefsValidateBreakPoints: The purpose of this function is to validate 
+ * all user breakpoint and color changes to heatmap data layer properties. When the  
+ * first error is found, an error  message (string array containing error information) 
+ * is created and returned to the prefsApply function. 
+ **********************************************************************************/
+function prefsValidateBreakPoints(colorMapName,prefPanel) {
+	var colorMap = heatMap.getColorMapManager().getColorMap(colorMapName);
+	var thresholds = colorMap.getThresholds();
+	var colors = colorMap.getColors();
+	var dupeBreak = false;
+	var breakOrder = false;
+	var prevBreakValue = -99999;
+	var errorMsg = null;
+	//Loop thru colormap thresholds and validate for order and duplicates
+	for (var i = 0; i < thresholds.length; i++) {
+		var breakElement = document.getElementById(colorMapName+"_breakPt"+i+"_breakPref");
+		//If current breakpoint is not greater than previous, throw order error
+		if (parseInt(breakElement.value) < prevBreakValue) {
+			breakOrder = true;
+			break;
+		}
+		//Loop thru thresholds, skipping current element, searching for a match to the 
+		//current selection.  If found, throw duplicate error
+		for (var j = 0; j < thresholds.length; j++) {
+			var be = document.getElementById(colorMapName+"_breakPt"+j+"_breakPref");
+			if (i != j) {
+				if (breakElement.value === be.value) {
+					dupeBreak = true;
+					break;
+				}
+			}
+		}
+	}
+	if (breakOrder) {
+		errorMsg =  [colorMapName, prefPanel, "ERROR: Data layer breakpoints must be in order"];
+	}
+	if (dupeBreak) {
+		errorMsg =  [colorMapName, prefPanel, "ERROR: Duplicate data layer breakpoint found above"];
+	}
+	return errorMsg;
+}
+
+/**********************************************************************************
+ * FUNCTION - prefsValidateBreakColors: The purpose of this function is to validate 
+ * all user color changes to heatmap classification and data layer properties. When the  
+ * first error is found, an error  message (string array containing error information) 
+ * is created and returned to the prefsApply function. 
+ **********************************************************************************/
+function prefsValidateBreakColors(colorMapName,prefPanel) {
+	var colorMap = heatMap.getColorMapManager().getColorMap(colorMapName);
+	var thresholds = colorMap.getThresholds();
+	var colors = colorMap.getColors();
+	var dupeColor = false;
+	for (var i = 0; i < colors.length; i++) {
+		var colorElement = document.getElementById(colorMapName+"_color"+i+"_colorPref");
+		for (var j = 0; j < thresholds.length; j++) {
+			var ce = document.getElementById(colorMapName+"_color"+j+"_colorPref");
+			if (i != j) {
+				if (colorElement.value === ce.value) {
+					dupeColor = true;
+					break;
+				}
+			}
+		}
+	}
+	if (dupeColor) {
+		return [colorMapName, prefPanel, "ERROR: Duplicate color setting found above"];
+	}
+	return null;
+}
+
+/**********************************************************************************
+ * FUNCTION - prefsApplyBreaks: The purpose of this function is to apply all 
+ * user entered changes to colors and breakpoints. 
+ **********************************************************************************/
+function prefsApplyBreaks(colorMapName, colorMapType, show) {
+	var colorMap = heatMap.getColorMapManager().getColorMap(colorMapName);
+	if (show) {
+		var thresholds = colorMap.getThresholds();
+		var colors = colorMap.getColors();
+		var newColors = getNewBreakColors(colorMapName);
+		colorMap.setColors(newColors);
+		if (colorMapType === "datalayer") {
+			var newThresholds = getNewBreakThresholds(colorMapName);
+			colorMap.setThresholds(newThresholds);
+		}
+		var missingElement = document.getElementById(colorMapName+"_missing_colorPref");
+		colorMap.setMissingColor(missingElement.value);
+		var colorMapMgr = heatMap.getColorMapManager();
+		colorMapMgr.setColorMap(colorMapName, colorMap);
+	}
+}
+
+/**********************************************************************************
+ * FUNCTION - getNewBreakColors: The purpose of this function is to grab all user
+ * color entries for a given colormap and place them on a string array.  It will 
+ * iterate thru the screen elements, pulling the current color entry for each 
+ * element, placing it in a new array, and returning that array. This function is 
+ * called by the prefsApplyBreaks function.  It is ALSO called from the data layer
+ * addLayerBreak and deleteLayerBreak functions with parameters passed in for 
+ * the position to add/delete and the action to be performed (add/delete).
+ **********************************************************************************/
+function getNewBreakColors(colorMapName, pos, action) {
+	var colorMap = heatMap.getColorMapManager().getColorMap(colorMapName);
+	var thresholds = colorMap.getThresholds();
+	var newColors = [];
+	for (var j = 0; j < thresholds.length; j++) {
+		var colorElement = document.getElementById(colorMapName+"_color"+j+"_colorPref");
+		//If being called from addLayerBreak or deleteLayerBreak
+		if (typeof pos !== 'undefined') {
+			if (action === "add") {
+				newColors.push(colorElement.value);
+				if (j === pos) {
+					newColors.push(colorElement.value);
+				}
+			} else {
+				if (j !== pos) {
+					newColors.push(colorElement.value);
+				}
+			}
+		} else {
+			newColors.push(colorElement.value);
+		}
+	}
+	return newColors;
+}
+
+/**********************************************************************************
+ * FUNCTION - getNewBreakThresholds: The purpose of this function is to grab all user
+ * data layer breakpoint entries for a given colormap and place them on a string array.  
+ * It will  iterate thru the screen elements, pulling the current breakpoint entry for each 
+ * element, placing it in a new array, and returning that array. This function is 
+ * called by the prefsApplyBreaks function (only for data layers).  It is ALSO called 
+ * from the data layer addLayerBreak and deleteLayerBreak functions with parameters 
+ * passed in for the position to add/delete and the action to be performed (add/delete).
+ **********************************************************************************/
+function getNewBreakThresholds(colorMapName, pos, action) {
+	var colorMap = heatMap.getColorMapManager().getColorMap(colorMapName);
+	var thresholds = colorMap.getThresholds();
+	var newThresholds = [];
+	for (var j = 0; j < thresholds.length; j++) {
+		var breakElement = document.getElementById(colorMapName+"_breakPt"+j+"_breakPref");
+		if (typeof pos !== 'undefined') {
+			if (action === "add") {
+				newThresholds.push(breakElement.value);
+				if (j === pos) {
+					newThresholds.push(breakElement.value);
+				}
+			} else {
+				if (j !== pos) {
+					newThresholds.push(breakElement.value);
+				}
+			}
+		} else {
+			newThresholds.push(breakElement.value);
+		}
+	}
+	return newThresholds;
+}
+
+/*===================================================================================
+  *  DATA LAYER PREFERENCE PROCESSING FUNCTIONS
+  *  
+  *  The following functions are utilized to present heat map data layer 
+  *  configuration options:
+  *  	- setupLayerPrefs
+  *  	- setupLayerBreaks
+  *  	- setupLayerPrefs
+  *     - addLayerBreak
+  *     - deleteLayerBreak
+  *     - reloadLayerBreaksColorMap
+  =================================================================================*/
+
+/**********************************************************************************
+ * FUNCTION - setupLayerPrefs: The purpose of this function is to construct a DIV 
+ * panel containing all data layer preferences.  A dropdown list containing all 
+ * data layers is presented and individual DIVs for each data layer, containing 
+ * breakpoints/colors, are added.
+ **********************************************************************************/
+function setupLayerPrefs(e, prefprefs){
+	var layerprefs = getDivElement("layerPrefs");
+	var prefContents = document.createElement("TABLE");
+	var colorMapName = "dl1";
+	var colorMap = heatMap.getColorMapManager().getColorMap(colorMapName);
+	prefContents.insertRow().innerHTML = formatBlankRow();
+	// TODO Future: primary and flick data layers in dropdown
+	var dlSelect = "<select name='dlPref_list' id='dlPref_list' onchange='showDlBreak();'><option value='dl1'>Data Layer 1</option></select>"
+	setTableRow(prefContents,["Data Layer: ", dlSelect]);
+	prefContents.insertRow().innerHTML = formatBlankRow();
+	prefContents.insertRow().innerHTML = formatBlankRow();
+	layerprefs.appendChild(prefContents);
+	var breakprefs = setupLayerBreaks(e, colorMapName, colorMapName);
+	breakprefs.style.display="block";
+	breakprefs.style.width = 300;
+	layerprefs.appendChild(breakprefs);
+	maxRows = maxRows+3;
+	// TODO Future: loop for primary and flick data layers
+	return layerprefs;
+}
+
+/**********************************************************************************
+ * FUNCTION - setupLayerBreaks: The purpose of this function is to construct a DIV 
+ * containing a list of breakpoints/colors for a given matrix data layer.
+ **********************************************************************************/
+function setupLayerBreaks(e, mapName, barName, barType){
+	var classBars = heatMap.getClassifications();
+	var colorMap = heatMap.getColorMapManager().getColorMap(mapName);
+	var thresholds = colorMap.getThresholds();
+	var colors = colorMap.getColors();
+	var helpprefs = getDivElement("breakPrefs_"+mapName);
+	var prefContents = document.createElement("TABLE"); 
+	var rowCtr = 0;
+	prefContents.insertRow().innerHTML = formatBlankRow();
+	prefContents.insertRow().innerHTML = formatBlankRow();
+	rowCtr++;
+	setTableRow(prefContents, ["<u>Breakpoint</u>", "<b><u>"+"Color"+"</u></b>","&nbsp;"]); 
+	rowCtr++;
+	for (var j = 0; j < thresholds.length; j++) {
+		var threshold = thresholds[j];
+		var color = colors[j];
+		var threshId = mapName+"_breakPt"+j;
+		var colorId = mapName+"_color"+j;
+		var breakPtInput = "<input name='"+threshId+"_breakPref' id='"+threshId+"_breakPref' value='"+threshold+"' maxlength='4' size='4'>";
+		var colorInput = "<input class='spectrumColor' type='color' name='"+colorId+"_colorPref' id='"+colorId+"_colorPref' value='"+color+"'>"; 
+		var addButton = "<img id='"+threshId+"_breakAdd' src='/plugins/visualizations/mda_heatmap_viz/static/images/plusButton.png' alt='Add Breakpoint' onclick='addLayerBreak("+j+",\""+mapName+"\");' align='top'/>"
+		var delButton = "<img id='"+threshId+"_breakDel' src='/plugins/visualizations/mda_heatmap_viz/static/images/minusButton.png' alt='Remove Breakpoint' onclick='deleteLayerBreak("+j+",\""+mapName+"\");' align='top'/>"
+		if (j === 0) {
+			setTableRow(prefContents, [breakPtInput, colorInput, addButton]);
+		} else {
+			setTableRow(prefContents, [breakPtInput,  colorInput, addButton, delButton]);
+		}
+		rowCtr++;
+	} 
+	prefContents.insertRow().innerHTML = formatBlankRow();
+	rowCtr++;
+	setTableRow(prefContents, ["Missing Color:",  "<input class='spectrumColor' type='color' name='"+mapName+"_missing_colorPref' id='"+mapName+"_missing_colorPref' value='"+colorMap.getMissingColor()+"'>"]);
+	rowCtr++;
+	if (rowCtr > maxRows) {
+		maxRows = rowCtr;
+	}
+	helpprefs.style.height = rowCtr;
+	helpprefs.style.width = 30;
+	helpprefs.appendChild(prefContents);
+	return helpprefs;
+}	
+
+/**********************************************************************************
+ * FUNCTION - addLayerBreak: The purpose of this function is to add a breakpoint
+ * row to a data layer colormap. A new row is created using the preceding row as a 
+ * template (i.e. breakpt value and color same as row clicked on).  
+ **********************************************************************************/
+function addLayerBreak(pos,colorMapName) {
+	//Retrieve colormap for data layer
+	var colorMap = heatMap.getColorMapManager().getColorMap(colorMapName);
+	var newThresholds = getNewBreakThresholds(colorMapName, pos,"add");
+	var newColors = getNewBreakColors(colorMapName, pos,"add");
+	//Calculate new size of data layers panel and reset size of the 
+	// entire preferences dialog (if necessary)
+	var layerRows = newThresholds.length+helpRowSize;
+	maxRows = Math.max(maxRows,layerRows);
+	setPrefsDivSizing();
+	//Apply new arrays for thresholds and colors to the datalayer
+	//and reload the colormap.
+	colorMap.setThresholds(newThresholds);
+	colorMap.setColors(newColors);
+	reloadLayerBreaksColorMap(colorMapName, colorMap);
+}
+
+/**********************************************************************************
+ * FUNCTION - deleteLayerBreak: The purpose of this function is to remove a breakpoint
+ * row from a data layer colormap.   
+ **********************************************************************************/
+function deleteLayerBreak(pos,colorMapName) {
+	var colorMap = heatMap.getColorMapManager().getColorMap(colorMapName);
+	var thresholds = colorMap.getThresholds();
+	var colors = colorMap.getColors();
+	var newThresholds = getNewBreakThresholds(colorMapName, pos,"delete");
+	var newColors = getNewBreakColors(colorMapName, pos,"delete");
+	//Apply new arrays for thresholds and colors to the datalayer
+	//and reload the colormap.
+	colorMap.setThresholds(newThresholds);
+	colorMap.setColors(newColors);
+	reloadLayerBreaksColorMap(colorMapName, colorMap);
+}
+
+/**********************************************************************************
+ * FUNCTION - reloadLayerBreaksColorMap: The purpose of this function is to reload
+ * the colormap for a given data layer.  The add/deleteLayerBreak methods call
+ * this common function.  The layerPrefs DIV is retrieved and the setupLayerBreaks
+ * method is called, passing in the newly edited colormap. 
+ **********************************************************************************/
+function reloadLayerBreaksColorMap(colorMapName, colorMap) {
+	var e = document.getElementById('gear_btn')
+	var colorMapMgr = heatMap.getColorMapManager();
+	colorMapMgr.setColorMap(colorMapName, colorMap);
+	var breakPrefs = document.getElementById('breakPrefs_'+colorMapName);
+	if (breakPrefs){
+		breakPrefs.remove();
+	}
+	var layerprefs = getDivElement("layerPrefs");
+	var breakPrefs = setupLayerBreaks(e, colorMapName, colorMapName);
+	breakPrefs.style.display="block";
+	breakPrefs.style.width = 300;
+	layerPrefs.appendChild(breakPrefs);
+}
+
+/*===================================================================================
+ *  COVARIATE CLASSIFICATION PREFERENCE PROCESSING FUNCTIONS
+ *  
+ *  The following functions are utilized to present heat map covariate classfication
+ *  bar configuration options:
+ *  	- setupClassPrefs
+ *  	- setupClassBreaks
+ *  	- setupAllClassesPrefs
+ *      - showAllBars
+ *      - setShowAll
+ =================================================================================*/
+
+/**********************************************************************************
+ * FUNCTION - setupClassPrefs: The purpose of this function is to construct a DIV 
+ * panel containing all covariate bar preferences.  A dropdown list containing all 
+ * covariate classification bars is presented and individual DIVs for each data layer, 
+ * containing  breakpoints/colors, are added. Additionally, a "front panel" DIV is
+ * created for "ALL" classification bars that contains preferences that are global
+ * to all of the individual bars.
+ **********************************************************************************/
+function setupClassPrefs(e, prefprefs){
+	var classBars = heatMap.getClassifications();
+	var classprefs = getDivElement("classPrefs");
+	var prefContents = document.createElement("TABLE");
+	prefContents.insertRow().innerHTML = formatBlankRow();
+	var filterInput = "<input name='all_searchPref' id='all_searchPref'>";
+	var filterButton = "<img id='all_searchPref_btn' src='/plugins/visualizations/mda_heatmap_viz/static/images/filterClassButton.png' alt='Search Covariates' onclick='filterClassPrefs(true);' align='top'/>";
+	if (filterVal != null) {
+		var filterInput = "<input name='all_searchPref' id='all_searchPref' value='"+filterVal+"'>";
+		var filterButton = "<img id='all_searchPref_btn' src='/plugins/visualizations/mda_heatmap_viz/static/images/removeFilterClassButton.png' alt='Search Covariates' onclick='filterClassPrefs(false);' align='top'/>";
+	}
+	var searchClasses = filterInput+"&nbsp;&nbsp;"+filterButton;
+	setTableRow(prefContents,[searchClasses], 4, 'right');
+	prefContents.insertRow().innerHTML = formatBlankRow();
+	prefContents.insertRow().innerHTML = formatBlankRow();
+	var classSelect = "<select name='classPref_list' id='classPref_list' onchange='showClassBreak();'>"
+    classSelect = classSelect+"<option value='ALL'>ALL</option>";
+	for (var key in classBars){
+		if (filterShow(key)) {
+			classSelect = classSelect+"<option value='"+classBars[key].colorScheme+"'>"+key+"</option>";
+		}
+	}
+	classSelect = classSelect+"</select>"
+	setTableRow(prefContents,["Covariate Bar: ", classSelect]);
+	prefContents.insertRow().innerHTML = formatBlankRow();
+	classprefs.appendChild(prefContents);
+	var i = 0;
+	for (var key in classBars){
+		if (filterShow(key)) {
+			var breakprefs = setupClassBreaks(e, classBars[key].colorScheme, key);
+			breakprefs.style.display="none";
+			breakprefs.style.width = 300;
+			classprefs.appendChild(breakprefs);
+		}
+		i++;
+	}
+	// Append a DIV panel for all of the covariate class bars 
+	var allPrefs = setupAllClassesPrefs(e); 
+	allPrefs.style.display="block";
+	classprefs.appendChild(allPrefs);
+	return classprefs;
+}
+
+/**********************************************************************************
+ * FUNCTION - setupClassBreaks: The purpose of this function is to construct a DIV 
+ * containing a list of all covariate bars with informational data and user preferences 
+ * that are common to all bars (show/hide and size).  
+ **********************************************************************************/
+function setupAllClassesPrefs(e){
+	var allprefs = getDivElement("breakPrefs_ALL");
+	var prefContents = document.createElement("TABLE");
+	var rowCtr = 0;
+	prefContents.insertRow().innerHTML = formatBlankRow();  
+	var colShowAll = "<input name='all_showPref' id='all_showPref' type='checkbox' onchange='showAllBars();'> ";
+	setTableRow(prefContents,["<u>"+"Classification"+"</u>", "<b><u>"+"Position"+"</u></b>", colShowAll+"<b><u>"+"Show"+"</u></b>", "<b><u>"+"Height"+"</u></b>"]);
+	rowCtr=2;
+	var classBars = heatMap.getClassifications();
+	var checkState = true;
+	for (var key in classBars){
+		if (filterShow(key)) {
+			var colShow = "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<input name='"+key+"_showPref' id='"+key+"_showPref' type='checkbox' onchange='setShowAll();'";
+			if (classBars[key].show == 'Y') {
+				colShow = colShow+"checked"
+			}
+			colShow = colShow+ " >";
+			var colHeight = "<input name='"+key+"_heightPref' id='"+key+"_heightPref' value='"+classBars[key].height+"' maxlength='2' size='2'>";
+			setTableRow(prefContents,[key,toTitleCase(classBars[key].position),colShow,colHeight]); 
+			rowCtr++;
+		}
+	}
+	allprefs.appendChild(prefContents);
+	if (rowCtr > maxRows) {
+		maxRows = rowCtr;
+	}
+	return allprefs;
+}	
+
+/**********************************************************************************
+ * FUNCTION - setupClassBreaks: The purpose of this function is to construct a DIV 
+ * containing a set informational data and a list of categories/colors for a given
+ * covariate classfication bar.  
+ **********************************************************************************/
+function setupClassBreaks(e, mapName, barName){
+	var classBars = heatMap.getClassifications();
+	var colorMap = heatMap.getColorMapManager().getColorMap(mapName);
+	var thresholds = colorMap.getThresholds();
+	var colors = colorMap.getColors();
+	var helpprefs = getDivElement("breakPrefs_"+mapName);
+	var prefContents = document.createElement("TABLE"); 
+	var rowCtr = 0;
+	prefContents.insertRow().innerHTML = formatBlankRow();
+	var colShow = "<input name='"+barName+"_showPref' id='"+barName+"_showPref' type='checkbox' ";
+	if (classBars[barName].show == 'Y') {
+		colShow = colShow+"checked"
+	}
+	colShow = colShow+ " >";
+	var colHeight = "<input name='"+barName+"_heightPref' id='"+barName+"_heightPref' value='"+classBars[barName].height+"' maxlength='2' size='2'>";
+	var pos = toTitleCase(classBars[barName].position);
+	var typ = toTitleCase(colorMap.getType());
+	if (classBars[barName].position == "row") {
+		pos = "Row";
+	}
+	setTableRow(prefContents,["Bar Position: ","<b>"+pos+"</b>"]);
+	setTableRow(prefContents,["Bar Type: ","<b>"+typ+"</b>"]);
+	prefContents.insertRow().innerHTML = formatBlankRow();
+	prefContents.insertRow().innerHTML = formatBlankRow();
+	rowCtr = rowCtr+4;
+	prefContents.insertRow().innerHTML = formatBlankRow();
+	rowCtr++;
+	setTableRow(prefContents, ["<u>Category</u>", "<b><u>"+"Color"+"</u></b>"]); 
+	rowCtr++;
+	for (var j = 0; j < thresholds.length; j++) {
+		var threshold = thresholds[j];
+		var color = colors[j];
+		var threshId = mapName+"_breakPt"+j;
+		var colorId = mapName+"_color"+j;
+		var colorInput = "<input class='spectrumColor' type='color' name='"+colorId+"_colorPref' id='"+colorId+"_colorPref' value='"+color+"'>"; 
+		setTableRow(prefContents, [threshold, colorInput]);
+		rowCtr++;
+	} 
+	prefContents.insertRow().innerHTML = formatBlankRow();
+	rowCtr++;
+	setTableRow(prefContents, ["Missing Color:",  "<input class='spectrumColor' type='color' name='"+mapName+"_missing_colorPref' id='"+mapName+"_missing_colorPref' value='"+colorMap.getMissingColor()+"'>"]);
+	rowCtr++;
+	if (rowCtr > maxRows) {
+		maxRows = rowCtr;
+	}
+	helpprefs.style.height = rowCtr;
+	helpprefs.style.width = 30;
+	helpprefs.appendChild(prefContents);
+	return helpprefs;
+}	
+
+/**********************************************************************************
+ * FUNCTION - showAllBars: The purpose of this function is to set the condition of
+ * the "show" checkbox for all covariate bars on the covariate bars tab of the user 
+ * preferences dialog. These checkboxes are located on the DIV that is visible when 
+ * the ALL entry of the covariate dropdown is selected. Whenever a  user checks the 
+ * show all box, all other boxes are checked.  
+ **********************************************************************************/
+function showAllBars(){
+	var classBars = heatMap.getClassifications();
+	var showAllBox = document.getElementById('all_showPref');
+	var checkState = false;
+	if (showAllBox.checked) {
+		checkState = true;
+	}
+	for (var key in classBars){
+		if (filterShow(key)) {
+			var colShow = document.getElementById(key+'_showPref');
+			colShow.checked = checkState;
+		}
+	}
+	return;
+}	
+
+/**********************************************************************************
+ * FUNCTION - setShowAll: The purpose of this function is to set the condition of
+ * the "show all" checkbox on the covariate bars tab of the user preferences dialog.
+ * This checkbox is located on the DIV that is visible when the ALL entry of the 
+ * covariate dropdown is selected. If a user un-checks a single box in the list of 
+ * covariate bars, the show all box is un-checked. Conversely, if a user checks a box 
+ * resulting in all of the boxes being selected, the show all box will be checked.
+ **********************************************************************************/
+function setShowAll(){
+	var classBars = heatMap.getClassifications();
+	var checkState = true;
+	for (var key in classBars){
+		var colShow = document.getElementById(key+'_showPref');
+		if (filterShow(key)) {
+			if (!colShow.checked) {
+				checkState = false;
+				break;
+			}
+		}
+	}
+	var showAllBox = document.getElementById('all_showPref');
+	showAllBox.checked = checkState;
+	return;
+}	
+
+
+/**********************************************************************************
+ * FUNCTION - showClassBreak: The purpose of this function is to show the 
+ * appropriate classification bar panel based upon the user selection of the 
+ * covariate dropdown on the covariates tab of the preferences screen.  This 
+ * function is also called when an error is trappped, opening the covariate DIV
+ * that contains the erroneous data entry.
+ **********************************************************************************/
+function showClassBreak(selClass) {
+	var classBtn = document.getElementById("classPref_list");
+	if (typeof selClass != 'undefined') {
+		classBtn.value = selClass;
+	} 
+	for (var i=0; i<classBtn.length; i++){
+		var classVal = "breakPrefs_"+classBtn.options[i].value;
+		var classDiv = document.getElementById(classVal);
+		var classSel = classBtn.options[i].selected;
+		if (classSel) {
+			classDiv.style.display = "block";
+		} else {
+			classDiv.style.display = "none";
+		}
+	}
+}
+
+/**********************************************************************************
+ * FUNCTION - filterClassPrefs: The purpose of this function is to initiate the 
+ * process of filtering option choices for classifications. It is fired when either
+ * the "Filter Covariates" or "Clear Filters" button is pressed on the covariates 
+ * preferences dialog.  The global filter value variable is set when filtering and 
+ * cleared when clearing and the editPreferences function is called to reload all
+ * preferences.
+ **********************************************************************************/
+function filterClassPrefs(filterOn){
+	searchPerformed = true;
+	if (filterOn) {
+		var searchPrefSelect = document.getElementById('all_searchPref');
+		var searchPrefVal = searchPrefSelect.value;
+		if (searchPrefVal != "") {
+			filterVal = searchPrefVal;
+		} else {
+			filterVal = null;
+		}
+	} else {
+		filterVal = null;
+	}
+	var prefspanel = document.getElementById('prefsPanel');
+	if (prefspanel){
+		prefspanel.remove();
+	}
+	editPreferences(document.getElementById('gear_btn'));
+}
+
+/**********************************************************************************
+ * FUNCTION - filterShow: The purpose of this function is to determine whether a 
+ * given covariates bar is to be shown given the state of the covariates filter
+ * search text box.
+ **********************************************************************************/
+function filterShow(key) {
+	var filterShow = false;
+	var lowerkey = key.toLowerCase();
+	if (filterVal != null) {
+		if (lowerkey.indexOf(filterVal.toLowerCase()) >= 0) {
+			filterShow = true;
+		}
+	} else {
+		filterShow = true;
+	}
+	return filterShow;
+	
+}
+
+/*===================================================================================
+ *  ROW COLUMN PREFERENCE PROCESSING FUNCTIONS
+ *  
+ *  The following functions are utilized to present heat map covariate classfication
+ *  bar configuration options:
+ *  	- setupRowColPrefs
+ *  	- showDendroSelections
+ *      - dendroRowShowChange
+ *      - dendroColShowChange
+ =================================================================================*/
+
+/**********************************************************************************
+ * FUNCTION - setupRowColPrefs: The purpose of this function is to construct a DIV 
+ * panel containing all row & col preferences.  Two sections are presented, one for
+ * rows and the other for cols.  Informational data begins each section and 
+ * properties for modifying the appearance of row/col dendograms appear at the end.
+ **********************************************************************************/
+function setupRowColPrefs(e, prefprefs){
+	var rowcolprefs = getDivElement("rowsColsPrefs");
+	var prefContents = document.createElement("TABLE");
+	prefContents.insertRow().innerHTML = formatBlankRow();
+	setTableRow(prefContents,["ROW INFORMATION:"], 2);
+	prefContents.insertRow().innerHTML = formatBlankRow();
+	var rowLabels = heatMap.getRowLabels();
+	var dendrogram = heatMap.getDendrogram();
+	var rowOrder = rowLabels['order_method'];
+	setTableRow(prefContents,["&nbsp;&nbsp;Labels Type:",rowLabels['label_type']]);
+	setTableRow(prefContents,["&nbsp;&nbsp;Ordering Method:",rowOrder]);
+	var rowCtr = 5;
+	var dendroShowOptions = "<option value='ALL'>Summary and Detail</option><option value='SUMMARY'>Summary Only</option><option value='NONE'>Hide</option></select>";
+	var dendroHeightOptions = "<option value='50'>50%</option><option value='75'>75%</option><option value='100'>100%</option><option value='125'>125%</option><option value='150'>150%</option><option value='200'>200%</option><option value='300'>300%</option></select>";
+	if (rowOrder === "Hierarchical") {
+		setTableRow(prefContents,["&nbsp;&nbsp;Agglomeration Method:",rowLabels['agglomeration_method']]);
+		rowCtr++;
+		setTableRow(prefContents,["&nbsp;&nbsp;Distance Metric:",rowLabels['distance_metric']]);
+		rowCtr++;
+		var rowDendroSelect = "<select name='rowDendroShowPref' id='rowDendroShowPref' onchange='dendroRowShowChange()'>"
+		rowDendroSelect = rowDendroSelect+dendroShowOptions;
+		setTableRow(prefContents,["&nbsp;&nbsp;Show Dendrogram:",rowDendroSelect]);  
+		rowCtr++;
+		var rowDendroHeightSelect = "<select name='rowDendroHeightPref' id='rowDendroHeightPref'>"
+		rowDendroHeightSelect = rowDendroHeightSelect+dendroHeightOptions;
+		setTableRow(prefContents,["&nbsp;&nbsp;Dendrogram Height:",rowDendroHeightSelect]); 
+		rowCtr++;
+	}  
+	prefContents.insertRow().innerHTML = formatBlankRow(); 
+	rowCtr++;
+	prefContents.insertRow().innerHTML = formatBlankRow();
+	rowCtr++;
+	setTableRow(prefContents,["COLUMN INFORMATION:"], 2);
+	rowCtr++;
+	prefContents.insertRow().innerHTML = formatBlankRow();
+	
+	var colLabels = heatMap.getColLabels();
+	var colOrder = colLabels['order_method'];
+	setTableRow(prefContents,["&nbsp;&nbsp;Labels Type:",colLabels['label_type']]);
+	rowCtr++;
+	setTableRow(prefContents,["&nbsp;&nbsp;Ordering Method:",colOrder]);
+	rowCtr++;
+	if (colOrder === "Hierarchical") {
+		setTableRow(prefContents,["&nbsp;&nbsp;Agglomeration Method:",colLabels['agglomeration_method']]);
+		rowCtr++;
+		setTableRow(prefContents,["&nbsp;&nbsp;Distance Metric:",colLabels['distance_metric']]);
+		rowCtr++;
+		var colDendroShowSelect = "<select name='colDendroShowPref' id='colDendroShowPref' onchange='dendroColShowChange()'>"
+		colDendroShowSelect = colDendroShowSelect+dendroShowOptions;
+		var colDendroHeightSelect = "<select name='colDendroHeightPref' id='colDendroHeightPref'>"
+		colDendroHeightSelect = colDendroHeightSelect+dendroHeightOptions;
+		setTableRow(prefContents,["&nbsp;&nbsp;Show Dendrogram:",colDendroShowSelect]);
+		rowCtr++;
+		setTableRow(prefContents,["&nbsp;&nbsp;Dendrogram Height:",colDendroHeightSelect]);
+		rowCtr++;
+	}
+	if (rowCtr > maxRows) {
+		maxRows = rowCtr;
+	}
+	rowcolprefs.appendChild(prefContents);
+	return rowcolprefs;
+}
+
+/**********************************************************************************
+ * FUNCTION - showDendroSelections: The purpose of this function is to set the 
+ * states of the row/column dendrogram show and height preferences.
+ **********************************************************************************/
+function showDendroSelections() {
+	var dendrogram = heatMap.getDendrogram();
+	var rowLabels = heatMap.getRowLabels();
+	var rowOrder = rowLabels['order_method'];
+	if (rowOrder === "Hierarchical") {
+		var dendroShowVal = dendrogram['row_dendro_show'];
+		document.getElementById("rowDendroShowPref").value = dendroShowVal;
+		var rowHeightPref = document.getElementById("rowDendroHeightPref");
+		if (dendroShowVal === 'NONE') {
+			var opt = rowHeightPref.options[6];
+			if (typeof opt != 'undefined') {
+				rowHeightPref.options[6].remove();
+			}
+			var option = document.createElement("option");
+			option.text = "NA";
+			option.value = '10';
+			rowHeightPref.add(option);
+			rowHeightPref.disabled = true;
+		}
+		rowHeightPref.value = dendrogram['row_dendro_height'];
+	}
+	var colLabels = heatMap.getColLabels();
+	var colOrder = colLabels['order_method'];
+	if (colOrder === "Hierarchical") {
+		var dendroShowVal = dendrogram['col_dendro_show'];
+		document.getElementById("colDendroShowPref").value = dendroShowVal;
+		var colHeightPref = document.getElementById("colDendroHeightPref");
+		if (dendroShowVal === 'NONE') {
+			var opt = colHeightPref.options[6];
+			if (typeof opt != 'undefined') {
+				colHeightPref.options[6].remove();
+			}
+			var option = document.createElement("option");
+			option.text = "NA";
+			option.value = '10';
+			colHeightPref.add(option);
+			colHeightPref.disabled = true;
+		}
+		colHeightPref.value = dendrogram['col_dendro_height'];
+	}
+}
+
+/**********************************************************************************
+ * FUNCTION - dendroRowShowChange: The purpose of this function is to respond to
+ * a change event on the show row dendrogram dropdown.  If the change is to Hide, 
+ * the row dendro height is set to 10 and the dropdown disabled. If the change is to
+ * one of the 2 Show options AND was previously Hide, set height to the default
+ * value of 100 and enable the dropdown.
+ **********************************************************************************/
+function dendroRowShowChange() {
+	var newValue = document.getElementById("rowDendroShowPref").value;
+	var rowHeightPref = document.getElementById("rowDendroHeightPref");
+	if (newValue === 'NONE') {
+		var option = document.createElement("option");
+		option.text = "NA";
+		option.value = '10';
+		rowHeightPref.add(option);
+		rowHeightPref.value = '10';
+		rowHeightPref.disabled = true;
+	} else if (rowHeightPref.disabled) {
+		var opt = rowHeightPref.options[6];
+		if (typeof opt != 'undefined') {
+			rowHeightPref.options[6].remove();
+		}
+		rowHeightPref.value = '100';
+		rowHeightPref.disabled = false;
+	}
+}
+
+/**********************************************************************************
+ * FUNCTION - dendroRowShowChange: The purpose of this function is to respond to
+ * a change event on the show row dendrogram dropdown.  If the change is to Hide, 
+ * the row dendro height is set to 10 and the dropdown disabled. If the change is to
+ * one of the 2 Show options AND was previously Hide, set height to the default
+ * value of 100 and enable the dropdown.
+ **********************************************************************************/
+function dendroColShowChange() {
+	var newValue = document.getElementById("colDendroShowPref").value;
+	var colHeightPref = document.getElementById("colDendroHeightPref");
+	if (newValue === 'NONE') {
+		var option = document.createElement("option");
+		option.text = "NA";
+		option.value = '10';
+		colHeightPref.add(option);
+		colHeightPref.value = '10';
+		colHeightPref.disabled = true;
+	} else if (colHeightPref.disabled) {
+		var opt = colHeightPref.options[6];
+		if (typeof opt != 'undefined') {
+			colHeightPref.options[6].remove();
+		}
+		colHeightPref.value = '100';
+		colHeightPref.disabled = false;
+	}
+}
+
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mda_heatmap_viz/static/javascript/custom.js	Thu Apr 07 14:43:37 2016 -0400
@@ -0,0 +1,42 @@
+/*
+ *  TO DO: give custom.js its own namespace so it can't modify variables used outside
+ *  Only outside function/variable it should be able to access right now is addLinkout and the inputs of each custom function (label text or index)
+ */
+
+addLinkout("Search Google", "Samples", "labels", searchGoogle);
+
+addLinkout("Search Google", "Genes", "labels", searchGoogle);
+addLinkout("Search GeneCards", "Genes", "labels", searchGeneCards);
+addLinkout("Search PubMed for All", "Genes", "labels", searchPubMedForAll);
+addLinkout("Search PubMed for Any", "Genes", "labels", searchPubMedForAny);
+
+function searchGoogle(selection, axis){
+	window.open('https://www.google.com/#q=' + selection.join("+"));
+}
+
+function searchGeneCards(labels){
+	var searchTerm = '';
+	for (var i = 0; i < labels.length; i++){
+		searchTerm += "+" + labels[i].split("|")[0];
+	}
+	searchTerm = searchTerm.substring(1);
+	window.open('http://www.genecards.org/Search/Keyword?queryString=' + searchTerm);
+}
+
+function searchPubMedForAll(labels){
+	var searchTerm = '';
+	for (var i = 0; i < labels.length; i++){
+		searchTerm += "+AND+" + labels[i].split("|")[0];
+	}
+	searchTerm = searchTerm.substring(5);
+	window.open("http://www.ncbi.nlm.nih.gov/pubmed/?term=" + searchTerm)
+}
+
+function searchPubMedForAny(labels){
+	var searchTerm = '';
+	for (var i = 0; i < labels.length; i++){
+		searchTerm += "+OR+" + labels[i].split("|")[0];
+	}
+	searchTerm = searchTerm.substring(4);
+	window.open("http://www.ncbi.nlm.nih.gov/pubmed/?term=" + searchTerm)
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mda_heatmap_viz/static/javascript/lib/deflate.js	Thu Apr 07 14:43:37 2016 -0400
@@ -0,0 +1,2060 @@
+/*
+ Copyright (c) 2013 Gildas Lormeau. All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+
+ 1. Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above copyright 
+ notice, this list of conditions and the following disclaimer in 
+ the documentation and/or other materials provided with the distribution.
+
+ 3. The names of the authors may not be used to endorse or promote products
+ derived from this software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
+ INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+ FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT,
+ INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT,
+ INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
+ OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+ EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/*
+ * This program is based on JZlib 1.0.2 ymnk, JCraft,Inc.
+ * JZlib is based on zlib-1.1.3, so all credit should go authors
+ * Jean-loup Gailly(jloup@gzip.org) and Mark Adler(madler@alumni.caltech.edu)
+ * and contributors of zlib.
+ */
+
+(function(global) {
+	"use strict";
+
+	// Global
+
+	var MAX_BITS = 15;
+	var D_CODES = 30;
+	var BL_CODES = 19;
+
+	var LENGTH_CODES = 29;
+	var LITERALS = 256;
+	var L_CODES = (LITERALS + 1 + LENGTH_CODES);
+	var HEAP_SIZE = (2 * L_CODES + 1);
+
+	var END_BLOCK = 256;
+
+	// Bit length codes must not exceed MAX_BL_BITS bits
+	var MAX_BL_BITS = 7;
+
+	// repeat previous bit length 3-6 times (2 bits of repeat count)
+	var REP_3_6 = 16;
+
+	// repeat a zero length 3-10 times (3 bits of repeat count)
+	var REPZ_3_10 = 17;
+
+	// repeat a zero length 11-138 times (7 bits of repeat count)
+	var REPZ_11_138 = 18;
+
+	// The lengths of the bit length codes are sent in order of decreasing
+	// probability, to avoid transmitting the lengths for unused bit
+	// length codes.
+
+	var Buf_size = 8 * 2;
+
+	// JZlib version : "1.0.2"
+	var Z_DEFAULT_COMPRESSION = -1;
+
+	// compression strategy
+	var Z_FILTERED = 1;
+	var Z_HUFFMAN_ONLY = 2;
+	var Z_DEFAULT_STRATEGY = 0;
+
+	var Z_NO_FLUSH = 0;
+	var Z_PARTIAL_FLUSH = 1;
+	var Z_FULL_FLUSH = 3;
+	var Z_FINISH = 4;
+
+	var Z_OK = 0;
+	var Z_STREAM_END = 1;
+	var Z_NEED_DICT = 2;
+	var Z_STREAM_ERROR = -2;
+	var Z_DATA_ERROR = -3;
+	var Z_BUF_ERROR = -5;
+
+	// Tree
+
+	// see definition of array dist_code below
+	var _dist_code = [ 0, 1, 2, 3, 4, 4, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
+			10, 10, 10, 10, 10, 10, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12,
+			12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13,
+			13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14,
+			14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14,
+			14, 14, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
+			15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 0, 0, 16, 17, 18, 18, 19, 19,
+			20, 20, 20, 20, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22, 22, 22, 23, 23, 23, 23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
+			24, 24, 24, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26,
+			26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27,
+			27, 27, 27, 27, 27, 27, 27, 27, 27, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28,
+			28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 29,
+			29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29,
+			29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29 ];
+
+	function Tree() {
+		var that = this;
+
+		// dyn_tree; // the dynamic tree
+		// max_code; // largest code with non zero frequency
+		// stat_desc; // the corresponding static tree
+
+		// Compute the optimal bit lengths for a tree and update the total bit
+		// length
+		// for the current block.
+		// IN assertion: the fields freq and dad are set, heap[heap_max] and
+		// above are the tree nodes sorted by increasing frequency.
+		// OUT assertions: the field len is set to the optimal bit length, the
+		// array bl_count contains the frequencies for each bit length.
+		// The length opt_len is updated; static_len is also updated if stree is
+		// not null.
+		function gen_bitlen(s) {
+			var tree = that.dyn_tree;
+			var stree = that.stat_desc.static_tree;
+			var extra = that.stat_desc.extra_bits;
+			var base = that.stat_desc.extra_base;
+			var max_length = that.stat_desc.max_length;
+			var h; // heap index
+			var n, m; // iterate over the tree elements
+			var bits; // bit length
+			var xbits; // extra bits
+			var f; // frequency
+			var overflow = 0; // number of elements with bit length too large
+
+			for (bits = 0; bits <= MAX_BITS; bits++)
+				s.bl_count[bits] = 0;
+
+			// In a first pass, compute the optimal bit lengths (which may
+			// overflow in the case of the bit length tree).
+			tree[s.heap[s.heap_max] * 2 + 1] = 0; // root of the heap
+
+			for (h = s.heap_max + 1; h < HEAP_SIZE; h++) {
+				n = s.heap[h];
+				bits = tree[tree[n * 2 + 1] * 2 + 1] + 1;
+				if (bits > max_length) {
+					bits = max_length;
+					overflow++;
+				}
+				tree[n * 2 + 1] = bits;
+				// We overwrite tree[n*2+1] which is no longer needed
+
+				if (n > that.max_code)
+					continue; // not a leaf node
+
+				s.bl_count[bits]++;
+				xbits = 0;
+				if (n >= base)
+					xbits = extra[n - base];
+				f = tree[n * 2];
+				s.opt_len += f * (bits + xbits);
+				if (stree)
+					s.static_len += f * (stree[n * 2 + 1] + xbits);
+			}
+			if (overflow === 0)
+				return;
+
+			// This happens for example on obj2 and pic of the Calgary corpus
+			// Find the first bit length which could increase:
+			do {
+				bits = max_length - 1;
+				while (s.bl_count[bits] === 0)
+					bits--;
+				s.bl_count[bits]--; // move one leaf down the tree
+				s.bl_count[bits + 1] += 2; // move one overflow item as its brother
+				s.bl_count[max_length]--;
+				// The brother of the overflow item also moves one step up,
+				// but this does not affect bl_count[max_length]
+				overflow -= 2;
+			} while (overflow > 0);
+
+			for (bits = max_length; bits !== 0; bits--) {
+				n = s.bl_count[bits];
+				while (n !== 0) {
+					m = s.heap[--h];
+					if (m > that.max_code)
+						continue;
+					if (tree[m * 2 + 1] != bits) {
+						s.opt_len += (bits - tree[m * 2 + 1]) * tree[m * 2];
+						tree[m * 2 + 1] = bits;
+					}
+					n--;
+				}
+			}
+		}
+
+		// Reverse the first len bits of a code, using straightforward code (a
+		// faster
+		// method would use a table)
+		// IN assertion: 1 <= len <= 15
+		function bi_reverse(code, // the value to invert
+		len // its bit length
+		) {
+			var res = 0;
+			do {
+				res |= code & 1;
+				code >>>= 1;
+				res <<= 1;
+			} while (--len > 0);
+			return res >>> 1;
+		}
+
+		// Generate the codes for a given tree and bit counts (which need not be
+		// optimal).
+		// IN assertion: the array bl_count contains the bit length statistics for
+		// the given tree and the field len is set for all tree elements.
+		// OUT assertion: the field code is set for all tree elements of non
+		// zero code length.
+		function gen_codes(tree, // the tree to decorate
+		max_code, // largest code with non zero frequency
+		bl_count // number of codes at each bit length
+		) {
+			var next_code = []; // next code value for each
+			// bit length
+			var code = 0; // running code value
+			var bits; // bit index
+			var n; // code index
+			var len;
+
+			// The distribution counts are first used to generate the code values
+			// without bit reversal.
+			for (bits = 1; bits <= MAX_BITS; bits++) {
+				next_code[bits] = code = ((code + bl_count[bits - 1]) << 1);
+			}
+
+			// Check that the bit counts in bl_count are consistent. The last code
+			// must be all ones.
+			// Assert (code + bl_count[MAX_BITS]-1 == (1<<MAX_BITS)-1,
+			// "inconsistent bit counts");
+			// Tracev((stderr,"\ngen_codes: max_code %d ", max_code));
+
+			for (n = 0; n <= max_code; n++) {
+				len = tree[n * 2 + 1];
+				if (len === 0)
+					continue;
+				// Now reverse the bits
+				tree[n * 2] = bi_reverse(next_code[len]++, len);
+			}
+		}
+
+		// Construct one Huffman tree and assigns the code bit strings and lengths.
+		// Update the total bit length for the current block.
+		// IN assertion: the field freq is set for all tree elements.
+		// OUT assertions: the fields len and code are set to the optimal bit length
+		// and corresponding code. The length opt_len is updated; static_len is
+		// also updated if stree is not null. The field max_code is set.
+		that.build_tree = function(s) {
+			var tree = that.dyn_tree;
+			var stree = that.stat_desc.static_tree;
+			var elems = that.stat_desc.elems;
+			var n, m; // iterate over heap elements
+			var max_code = -1; // largest code with non zero frequency
+			var node; // new node being created
+
+			// Construct the initial heap, with least frequent element in
+			// heap[1]. The sons of heap[n] are heap[2*n] and heap[2*n+1].
+			// heap[0] is not used.
+			s.heap_len = 0;
+			s.heap_max = HEAP_SIZE;
+
+			for (n = 0; n < elems; n++) {
+				if (tree[n * 2] !== 0) {
+					s.heap[++s.heap_len] = max_code = n;
+					s.depth[n] = 0;
+				} else {
+					tree[n * 2 + 1] = 0;
+				}
+			}
+
+			// The pkzip format requires that at least one distance code exists,
+			// and that at least one bit should be sent even if there is only one
+			// possible code. So to avoid special checks later on we force at least
+			// two codes of non zero frequency.
+			while (s.heap_len < 2) {
+				node = s.heap[++s.heap_len] = max_code < 2 ? ++max_code : 0;
+				tree[node * 2] = 1;
+				s.depth[node] = 0;
+				s.opt_len--;
+				if (stree)
+					s.static_len -= stree[node * 2 + 1];
+				// node is 0 or 1 so it does not have extra bits
+			}
+			that.max_code = max_code;
+
+			// The elements heap[heap_len/2+1 .. heap_len] are leaves of the tree,
+			// establish sub-heaps of increasing lengths:
+
+			for (n = Math.floor(s.heap_len / 2); n >= 1; n--)
+				s.pqdownheap(tree, n);
+
+			// Construct the Huffman tree by repeatedly combining the least two
+			// frequent nodes.
+
+			node = elems; // next internal node of the tree
+			do {
+				// n = node of least frequency
+				n = s.heap[1];
+				s.heap[1] = s.heap[s.heap_len--];
+				s.pqdownheap(tree, 1);
+				m = s.heap[1]; // m = node of next least frequency
+
+				s.heap[--s.heap_max] = n; // keep the nodes sorted by frequency
+				s.heap[--s.heap_max] = m;
+
+				// Create a new node father of n and m
+				tree[node * 2] = (tree[n * 2] + tree[m * 2]);
+				s.depth[node] = Math.max(s.depth[n], s.depth[m]) + 1;
+				tree[n * 2 + 1] = tree[m * 2 + 1] = node;
+
+				// and insert the new node in the heap
+				s.heap[1] = node++;
+				s.pqdownheap(tree, 1);
+			} while (s.heap_len >= 2);
+
+			s.heap[--s.heap_max] = s.heap[1];
+
+			// At this point, the fields freq and dad are set. We can now
+			// generate the bit lengths.
+
+			gen_bitlen(s);
+
+			// The field len is now set, we can generate the bit codes
+			gen_codes(tree, that.max_code, s.bl_count);
+		};
+
+	}
+
+	Tree._length_code = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 12, 12, 13, 13, 13, 13, 14, 14, 14, 14, 15, 15, 15, 15, 16, 16, 16, 16,
+			16, 16, 16, 16, 17, 17, 17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 18, 18, 19, 19, 19, 19, 19, 19, 19, 19, 20, 20, 20, 20, 20, 20, 20, 20, 20,
+			20, 20, 20, 20, 20, 20, 20, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22,
+			22, 22, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
+			24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
+			25, 25, 25, 25, 25, 25, 25, 25, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26,
+			26, 26, 26, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 28 ];
+
+	Tree.base_length = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 12, 14, 16, 20, 24, 28, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 0 ];
+
+	Tree.base_dist = [ 0, 1, 2, 3, 4, 6, 8, 12, 16, 24, 32, 48, 64, 96, 128, 192, 256, 384, 512, 768, 1024, 1536, 2048, 3072, 4096, 6144, 8192, 12288, 16384,
+			24576 ];
+
+	// Mapping from a distance to a distance code. dist is the distance - 1 and
+	// must not have side effects. _dist_code[256] and _dist_code[257] are never
+	// used.
+	Tree.d_code = function(dist) {
+		return ((dist) < 256 ? _dist_code[dist] : _dist_code[256 + ((dist) >>> 7)]);
+	};
+
+	// extra bits for each length code
+	Tree.extra_lbits = [ 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0 ];
+
+	// extra bits for each distance code
+	Tree.extra_dbits = [ 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13 ];
+
+	// extra bits for each bit length code
+	Tree.extra_blbits = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 3, 7 ];
+
+	Tree.bl_order = [ 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 ];
+
+	// StaticTree
+
+	function StaticTree(static_tree, extra_bits, extra_base, elems, max_length) {
+		var that = this;
+		that.static_tree = static_tree;
+		that.extra_bits = extra_bits;
+		that.extra_base = extra_base;
+		that.elems = elems;
+		that.max_length = max_length;
+	}
+
+	StaticTree.static_ltree = [ 12, 8, 140, 8, 76, 8, 204, 8, 44, 8, 172, 8, 108, 8, 236, 8, 28, 8, 156, 8, 92, 8, 220, 8, 60, 8, 188, 8, 124, 8, 252, 8, 2, 8,
+			130, 8, 66, 8, 194, 8, 34, 8, 162, 8, 98, 8, 226, 8, 18, 8, 146, 8, 82, 8, 210, 8, 50, 8, 178, 8, 114, 8, 242, 8, 10, 8, 138, 8, 74, 8, 202, 8, 42,
+			8, 170, 8, 106, 8, 234, 8, 26, 8, 154, 8, 90, 8, 218, 8, 58, 8, 186, 8, 122, 8, 250, 8, 6, 8, 134, 8, 70, 8, 198, 8, 38, 8, 166, 8, 102, 8, 230, 8,
+			22, 8, 150, 8, 86, 8, 214, 8, 54, 8, 182, 8, 118, 8, 246, 8, 14, 8, 142, 8, 78, 8, 206, 8, 46, 8, 174, 8, 110, 8, 238, 8, 30, 8, 158, 8, 94, 8,
+			222, 8, 62, 8, 190, 8, 126, 8, 254, 8, 1, 8, 129, 8, 65, 8, 193, 8, 33, 8, 161, 8, 97, 8, 225, 8, 17, 8, 145, 8, 81, 8, 209, 8, 49, 8, 177, 8, 113,
+			8, 241, 8, 9, 8, 137, 8, 73, 8, 201, 8, 41, 8, 169, 8, 105, 8, 233, 8, 25, 8, 153, 8, 89, 8, 217, 8, 57, 8, 185, 8, 121, 8, 249, 8, 5, 8, 133, 8,
+			69, 8, 197, 8, 37, 8, 165, 8, 101, 8, 229, 8, 21, 8, 149, 8, 85, 8, 213, 8, 53, 8, 181, 8, 117, 8, 245, 8, 13, 8, 141, 8, 77, 8, 205, 8, 45, 8,
+			173, 8, 109, 8, 237, 8, 29, 8, 157, 8, 93, 8, 221, 8, 61, 8, 189, 8, 125, 8, 253, 8, 19, 9, 275, 9, 147, 9, 403, 9, 83, 9, 339, 9, 211, 9, 467, 9,
+			51, 9, 307, 9, 179, 9, 435, 9, 115, 9, 371, 9, 243, 9, 499, 9, 11, 9, 267, 9, 139, 9, 395, 9, 75, 9, 331, 9, 203, 9, 459, 9, 43, 9, 299, 9, 171, 9,
+			427, 9, 107, 9, 363, 9, 235, 9, 491, 9, 27, 9, 283, 9, 155, 9, 411, 9, 91, 9, 347, 9, 219, 9, 475, 9, 59, 9, 315, 9, 187, 9, 443, 9, 123, 9, 379,
+			9, 251, 9, 507, 9, 7, 9, 263, 9, 135, 9, 391, 9, 71, 9, 327, 9, 199, 9, 455, 9, 39, 9, 295, 9, 167, 9, 423, 9, 103, 9, 359, 9, 231, 9, 487, 9, 23,
+			9, 279, 9, 151, 9, 407, 9, 87, 9, 343, 9, 215, 9, 471, 9, 55, 9, 311, 9, 183, 9, 439, 9, 119, 9, 375, 9, 247, 9, 503, 9, 15, 9, 271, 9, 143, 9,
+			399, 9, 79, 9, 335, 9, 207, 9, 463, 9, 47, 9, 303, 9, 175, 9, 431, 9, 111, 9, 367, 9, 239, 9, 495, 9, 31, 9, 287, 9, 159, 9, 415, 9, 95, 9, 351, 9,
+			223, 9, 479, 9, 63, 9, 319, 9, 191, 9, 447, 9, 127, 9, 383, 9, 255, 9, 511, 9, 0, 7, 64, 7, 32, 7, 96, 7, 16, 7, 80, 7, 48, 7, 112, 7, 8, 7, 72, 7,
+			40, 7, 104, 7, 24, 7, 88, 7, 56, 7, 120, 7, 4, 7, 68, 7, 36, 7, 100, 7, 20, 7, 84, 7, 52, 7, 116, 7, 3, 8, 131, 8, 67, 8, 195, 8, 35, 8, 163, 8,
+			99, 8, 227, 8 ];
+
+	StaticTree.static_dtree = [ 0, 5, 16, 5, 8, 5, 24, 5, 4, 5, 20, 5, 12, 5, 28, 5, 2, 5, 18, 5, 10, 5, 26, 5, 6, 5, 22, 5, 14, 5, 30, 5, 1, 5, 17, 5, 9, 5,
+			25, 5, 5, 5, 21, 5, 13, 5, 29, 5, 3, 5, 19, 5, 11, 5, 27, 5, 7, 5, 23, 5 ];
+
+	StaticTree.static_l_desc = new StaticTree(StaticTree.static_ltree, Tree.extra_lbits, LITERALS + 1, L_CODES, MAX_BITS);
+
+	StaticTree.static_d_desc = new StaticTree(StaticTree.static_dtree, Tree.extra_dbits, 0, D_CODES, MAX_BITS);
+
+	StaticTree.static_bl_desc = new StaticTree(null, Tree.extra_blbits, 0, BL_CODES, MAX_BL_BITS);
+
+	// Deflate
+
+	var MAX_MEM_LEVEL = 9;
+	var DEF_MEM_LEVEL = 8;
+
+	function Config(good_length, max_lazy, nice_length, max_chain, func) {
+		var that = this;
+		that.good_length = good_length;
+		that.max_lazy = max_lazy;
+		that.nice_length = nice_length;
+		that.max_chain = max_chain;
+		that.func = func;
+	}
+
+	var STORED = 0;
+	var FAST = 1;
+	var SLOW = 2;
+	var config_table = [ new Config(0, 0, 0, 0, STORED), new Config(4, 4, 8, 4, FAST), new Config(4, 5, 16, 8, FAST), new Config(4, 6, 32, 32, FAST),
+			new Config(4, 4, 16, 16, SLOW), new Config(8, 16, 32, 32, SLOW), new Config(8, 16, 128, 128, SLOW), new Config(8, 32, 128, 256, SLOW),
+			new Config(32, 128, 258, 1024, SLOW), new Config(32, 258, 258, 4096, SLOW) ];
+
+	var z_errmsg = [ "need dictionary", // Z_NEED_DICT
+	// 2
+	"stream end", // Z_STREAM_END 1
+	"", // Z_OK 0
+	"", // Z_ERRNO (-1)
+	"stream error", // Z_STREAM_ERROR (-2)
+	"data error", // Z_DATA_ERROR (-3)
+	"", // Z_MEM_ERROR (-4)
+	"buffer error", // Z_BUF_ERROR (-5)
+	"",// Z_VERSION_ERROR (-6)
+	"" ];
+
+	// block not completed, need more input or more output
+	var NeedMore = 0;
+
+	// block flush performed
+	var BlockDone = 1;
+
+	// finish started, need only more output at next deflate
+	var FinishStarted = 2;
+
+	// finish done, accept no more input or output
+	var FinishDone = 3;
+
+	// preset dictionary flag in zlib header
+	var PRESET_DICT = 0x20;
+
+	var INIT_STATE = 42;
+	var BUSY_STATE = 113;
+	var FINISH_STATE = 666;
+
+	// The deflate compression method
+	var Z_DEFLATED = 8;
+
+	var STORED_BLOCK = 0;
+	var STATIC_TREES = 1;
+	var DYN_TREES = 2;
+
+	var MIN_MATCH = 3;
+	var MAX_MATCH = 258;
+	var MIN_LOOKAHEAD = (MAX_MATCH + MIN_MATCH + 1);
+
+	function smaller(tree, n, m, depth) {
+		var tn2 = tree[n * 2];
+		var tm2 = tree[m * 2];
+		return (tn2 < tm2 || (tn2 == tm2 && depth[n] <= depth[m]));
+	}
+
+	function Deflate() {
+
+		var that = this;
+		var strm; // pointer back to this zlib stream
+		var status; // as the name implies
+		// pending_buf; // output still pending
+		var pending_buf_size; // size of pending_buf
+		// pending_out; // next pending byte to output to the stream
+		// pending; // nb of bytes in the pending buffer
+		var method; // STORED (for zip only) or DEFLATED
+		var last_flush; // value of flush param for previous deflate call
+
+		var w_size; // LZ77 window size (32K by default)
+		var w_bits; // log2(w_size) (8..16)
+		var w_mask; // w_size - 1
+
+		var window;
+		// Sliding window. Input bytes are read into the second half of the window,
+		// and move to the first half later to keep a dictionary of at least wSize
+		// bytes. With this organization, matches are limited to a distance of
+		// wSize-MAX_MATCH bytes, but this ensures that IO is always
+		// performed with a length multiple of the block size. Also, it limits
+		// the window size to 64K, which is quite useful on MSDOS.
+		// To do: use the user input buffer as sliding window.
+
+		var window_size;
+		// Actual size of window: 2*wSize, except when the user input buffer
+		// is directly used as sliding window.
+
+		var prev;
+		// Link to older string with same hash index. To limit the size of this
+		// array to 64K, this link is maintained only for the last 32K strings.
+		// An index in this array is thus a window index modulo 32K.
+
+		var head; // Heads of the hash chains or NIL.
+
+		var ins_h; // hash index of string to be inserted
+		var hash_size; // number of elements in hash table
+		var hash_bits; // log2(hash_size)
+		var hash_mask; // hash_size-1
+
+		// Number of bits by which ins_h must be shifted at each input
+		// step. It must be such that after MIN_MATCH steps, the oldest
+		// byte no longer takes part in the hash key, that is:
+		// hash_shift * MIN_MATCH >= hash_bits
+		var hash_shift;
+
+		// Window position at the beginning of the current output block. Gets
+		// negative when the window is moved backwards.
+
+		var block_start;
+
+		var match_length; // length of best match
+		var prev_match; // previous match
+		var match_available; // set if previous match exists
+		var strstart; // start of string to insert
+		var match_start; // start of matching string
+		var lookahead; // number of valid bytes ahead in window
+
+		// Length of the best match at previous step. Matches not greater than this
+		// are discarded. This is used in the lazy match evaluation.
+		var prev_length;
+
+		// To speed up deflation, hash chains are never searched beyond this
+		// length. A higher limit improves compression ratio but degrades the speed.
+		var max_chain_length;
+
+		// Attempt to find a better match only when the current match is strictly
+		// smaller than this value. This mechanism is used only for compression
+		// levels >= 4.
+		var max_lazy_match;
+
+		// Insert new strings in the hash table only if the match length is not
+		// greater than this length. This saves time but degrades compression.
+		// max_insert_length is used only for compression levels <= 3.
+
+		var level; // compression level (1..9)
+		var strategy; // favor or force Huffman coding
+
+		// Use a faster search when the previous match is longer than this
+		var good_match;
+
+		// Stop searching when current match exceeds this
+		var nice_match;
+
+		var dyn_ltree; // literal and length tree
+		var dyn_dtree; // distance tree
+		var bl_tree; // Huffman tree for bit lengths
+
+		var l_desc = new Tree(); // desc for literal tree
+		var d_desc = new Tree(); // desc for distance tree
+		var bl_desc = new Tree(); // desc for bit length tree
+
+		// that.heap_len; // number of elements in the heap
+		// that.heap_max; // element of largest frequency
+		// The sons of heap[n] are heap[2*n] and heap[2*n+1]. heap[0] is not used.
+		// The same heap array is used to build all trees.
+
+		// Depth of each subtree used as tie breaker for trees of equal frequency
+		that.depth = [];
+
+		var l_buf; // index for literals or lengths */
+
+		// Size of match buffer for literals/lengths. There are 4 reasons for
+		// limiting lit_bufsize to 64K:
+		// - frequencies can be kept in 16 bit counters
+		// - if compression is not successful for the first block, all input
+		// data is still in the window so we can still emit a stored block even
+		// when input comes from standard input. (This can also be done for
+		// all blocks if lit_bufsize is not greater than 32K.)
+		// - if compression is not successful for a file smaller than 64K, we can
+		// even emit a stored file instead of a stored block (saving 5 bytes).
+		// This is applicable only for zip (not gzip or zlib).
+		// - creating new Huffman trees less frequently may not provide fast
+		// adaptation to changes in the input data statistics. (Take for
+		// example a binary file with poorly compressible code followed by
+		// a highly compressible string table.) Smaller buffer sizes give
+		// fast adaptation but have of course the overhead of transmitting
+		// trees more frequently.
+		// - I can't count above 4
+		var lit_bufsize;
+
+		var last_lit; // running index in l_buf
+
+		// Buffer for distances. To simplify the code, d_buf and l_buf have
+		// the same number of elements. To use different lengths, an extra flag
+		// array would be necessary.
+
+		var d_buf; // index of pendig_buf
+
+		// that.opt_len; // bit length of current block with optimal trees
+		// that.static_len; // bit length of current block with static trees
+		var matches; // number of string matches in current block
+		var last_eob_len; // bit length of EOB code for last block
+
+		// Output buffer. bits are inserted starting at the bottom (least
+		// significant bits).
+		var bi_buf;
+
+		// Number of valid bits in bi_buf. All bits above the last valid bit
+		// are always zero.
+		var bi_valid;
+
+		// number of codes at each bit length for an optimal tree
+		that.bl_count = [];
+
+		// heap used to build the Huffman trees
+		that.heap = [];
+
+		dyn_ltree = [];
+		dyn_dtree = [];
+		bl_tree = [];
+
+		function lm_init() {
+			var i;
+			window_size = 2 * w_size;
+
+			head[hash_size - 1] = 0;
+			for (i = 0; i < hash_size - 1; i++) {
+				head[i] = 0;
+			}
+
+			// Set the default configuration parameters:
+			max_lazy_match = config_table[level].max_lazy;
+			good_match = config_table[level].good_length;
+			nice_match = config_table[level].nice_length;
+			max_chain_length = config_table[level].max_chain;
+
+			strstart = 0;
+			block_start = 0;
+			lookahead = 0;
+			match_length = prev_length = MIN_MATCH - 1;
+			match_available = 0;
+			ins_h = 0;
+		}
+
+		function init_block() {
+			var i;
+			// Initialize the trees.
+			for (i = 0; i < L_CODES; i++)
+				dyn_ltree[i * 2] = 0;
+			for (i = 0; i < D_CODES; i++)
+				dyn_dtree[i * 2] = 0;
+			for (i = 0; i < BL_CODES; i++)
+				bl_tree[i * 2] = 0;
+
+			dyn_ltree[END_BLOCK * 2] = 1;
+			that.opt_len = that.static_len = 0;
+			last_lit = matches = 0;
+		}
+
+		// Initialize the tree data structures for a new zlib stream.
+		function tr_init() {
+
+			l_desc.dyn_tree = dyn_ltree;
+			l_desc.stat_desc = StaticTree.static_l_desc;
+
+			d_desc.dyn_tree = dyn_dtree;
+			d_desc.stat_desc = StaticTree.static_d_desc;
+
+			bl_desc.dyn_tree = bl_tree;
+			bl_desc.stat_desc = StaticTree.static_bl_desc;
+
+			bi_buf = 0;
+			bi_valid = 0;
+			last_eob_len = 8; // enough lookahead for inflate
+
+			// Initialize the first block of the first file:
+			init_block();
+		}
+
+		// Restore the heap property by moving down the tree starting at node k,
+		// exchanging a node with the smallest of its two sons if necessary,
+		// stopping
+		// when the heap property is re-established (each father smaller than its
+		// two sons).
+		that.pqdownheap = function(tree, // the tree to restore
+		k // node to move down
+		) {
+			var heap = that.heap;
+			var v = heap[k];
+			var j = k << 1; // left son of k
+			while (j <= that.heap_len) {
+				// Set j to the smallest of the two sons:
+				if (j < that.heap_len && smaller(tree, heap[j + 1], heap[j], that.depth)) {
+					j++;
+				}
+				// Exit if v is smaller than both sons
+				if (smaller(tree, v, heap[j], that.depth))
+					break;
+
+				// Exchange v with the smallest son
+				heap[k] = heap[j];
+				k = j;
+				// And continue down the tree, setting j to the left son of k
+				j <<= 1;
+			}
+			heap[k] = v;
+		};
+
+		// Scan a literal or distance tree to determine the frequencies of the codes
+		// in the bit length tree.
+		function scan_tree(tree,// the tree to be scanned
+		max_code // and its largest code of non zero frequency
+		) {
+			var n; // iterates over all tree elements
+			var prevlen = -1; // last emitted length
+			var curlen; // length of current code
+			var nextlen = tree[0 * 2 + 1]; // length of next code
+			var count = 0; // repeat count of the current code
+			var max_count = 7; // max repeat count
+			var min_count = 4; // min repeat count
+
+			if (nextlen === 0) {
+				max_count = 138;
+				min_count = 3;
+			}
+			tree[(max_code + 1) * 2 + 1] = 0xffff; // guard
+
+			for (n = 0; n <= max_code; n++) {
+				curlen = nextlen;
+				nextlen = tree[(n + 1) * 2 + 1];
+				if (++count < max_count && curlen == nextlen) {
+					continue;
+				} else if (count < min_count) {
+					bl_tree[curlen * 2] += count;
+				} else if (curlen !== 0) {
+					if (curlen != prevlen)
+						bl_tree[curlen * 2]++;
+					bl_tree[REP_3_6 * 2]++;
+				} else if (count <= 10) {
+					bl_tree[REPZ_3_10 * 2]++;
+				} else {
+					bl_tree[REPZ_11_138 * 2]++;
+				}
+				count = 0;
+				prevlen = curlen;
+				if (nextlen === 0) {
+					max_count = 138;
+					min_count = 3;
+				} else if (curlen == nextlen) {
+					max_count = 6;
+					min_count = 3;
+				} else {
+					max_count = 7;
+					min_count = 4;
+				}
+			}
+		}
+
+		// Construct the Huffman tree for the bit lengths and return the index in
+		// bl_order of the last bit length code to send.
+		function build_bl_tree() {
+			var max_blindex; // index of last bit length code of non zero freq
+
+			// Determine the bit length frequencies for literal and distance trees
+			scan_tree(dyn_ltree, l_desc.max_code);
+			scan_tree(dyn_dtree, d_desc.max_code);
+
+			// Build the bit length tree:
+			bl_desc.build_tree(that);
+			// opt_len now includes the length of the tree representations, except
+			// the lengths of the bit lengths codes and the 5+5+4 bits for the
+			// counts.
+
+			// Determine the number of bit length codes to send. The pkzip format
+			// requires that at least 4 bit length codes be sent. (appnote.txt says
+			// 3 but the actual value used is 4.)
+			for (max_blindex = BL_CODES - 1; max_blindex >= 3; max_blindex--) {
+				if (bl_tree[Tree.bl_order[max_blindex] * 2 + 1] !== 0)
+					break;
+			}
+			// Update opt_len to include the bit length tree and counts
+			that.opt_len += 3 * (max_blindex + 1) + 5 + 5 + 4;
+
+			return max_blindex;
+		}
+
+		// Output a byte on the stream.
+		// IN assertion: there is enough room in pending_buf.
+		function put_byte(p) {
+			that.pending_buf[that.pending++] = p;
+		}
+
+		function put_short(w) {
+			put_byte(w & 0xff);
+			put_byte((w >>> 8) & 0xff);
+		}
+
+		function putShortMSB(b) {
+			put_byte((b >> 8) & 0xff);
+			put_byte((b & 0xff) & 0xff);
+		}
+
+		function send_bits(value, length) {
+			var val, len = length;
+			if (bi_valid > Buf_size - len) {
+				val = value;
+				// bi_buf |= (val << bi_valid);
+				bi_buf |= ((val << bi_valid) & 0xffff);
+				put_short(bi_buf);
+				bi_buf = val >>> (Buf_size - bi_valid);
+				bi_valid += len - Buf_size;
+			} else {
+				// bi_buf |= (value) << bi_valid;
+				bi_buf |= (((value) << bi_valid) & 0xffff);
+				bi_valid += len;
+			}
+		}
+
+		function send_code(c, tree) {
+			var c2 = c * 2;
+			send_bits(tree[c2] & 0xffff, tree[c2 + 1] & 0xffff);
+		}
+
+		// Send a literal or distance tree in compressed form, using the codes in
+		// bl_tree.
+		function send_tree(tree,// the tree to be sent
+		max_code // and its largest code of non zero frequency
+		) {
+			var n; // iterates over all tree elements
+			var prevlen = -1; // last emitted length
+			var curlen; // length of current code
+			var nextlen = tree[0 * 2 + 1]; // length of next code
+			var count = 0; // repeat count of the current code
+			var max_count = 7; // max repeat count
+			var min_count = 4; // min repeat count
+
+			if (nextlen === 0) {
+				max_count = 138;
+				min_count = 3;
+			}
+
+			for (n = 0; n <= max_code; n++) {
+				curlen = nextlen;
+				nextlen = tree[(n + 1) * 2 + 1];
+				if (++count < max_count && curlen == nextlen) {
+					continue;
+				} else if (count < min_count) {
+					do {
+						send_code(curlen, bl_tree);
+					} while (--count !== 0);
+				} else if (curlen !== 0) {
+					if (curlen != prevlen) {
+						send_code(curlen, bl_tree);
+						count--;
+					}
+					send_code(REP_3_6, bl_tree);
+					send_bits(count - 3, 2);
+				} else if (count <= 10) {
+					send_code(REPZ_3_10, bl_tree);
+					send_bits(count - 3, 3);
+				} else {
+					send_code(REPZ_11_138, bl_tree);
+					send_bits(count - 11, 7);
+				}
+				count = 0;
+				prevlen = curlen;
+				if (nextlen === 0) {
+					max_count = 138;
+					min_count = 3;
+				} else if (curlen == nextlen) {
+					max_count = 6;
+					min_count = 3;
+				} else {
+					max_count = 7;
+					min_count = 4;
+				}
+			}
+		}
+
+		// Send the header for a block using dynamic Huffman trees: the counts, the
+		// lengths of the bit length codes, the literal tree and the distance tree.
+		// IN assertion: lcodes >= 257, dcodes >= 1, blcodes >= 4.
+		function send_all_trees(lcodes, dcodes, blcodes) {
+			var rank; // index in bl_order
+
+			send_bits(lcodes - 257, 5); // not +255 as stated in appnote.txt
+			send_bits(dcodes - 1, 5);
+			send_bits(blcodes - 4, 4); // not -3 as stated in appnote.txt
+			for (rank = 0; rank < blcodes; rank++) {
+				send_bits(bl_tree[Tree.bl_order[rank] * 2 + 1], 3);
+			}
+			send_tree(dyn_ltree, lcodes - 1); // literal tree
+			send_tree(dyn_dtree, dcodes - 1); // distance tree
+		}
+
+		// Flush the bit buffer, keeping at most 7 bits in it.
+		function bi_flush() {
+			if (bi_valid == 16) {
+				put_short(bi_buf);
+				bi_buf = 0;
+				bi_valid = 0;
+			} else if (bi_valid >= 8) {
+				put_byte(bi_buf & 0xff);
+				bi_buf >>>= 8;
+				bi_valid -= 8;
+			}
+		}
+
+		// Send one empty static block to give enough lookahead for inflate.
+		// This takes 10 bits, of which 7 may remain in the bit buffer.
+		// The current inflate code requires 9 bits of lookahead. If the
+		// last two codes for the previous block (real code plus EOB) were coded
+		// on 5 bits or less, inflate may have only 5+3 bits of lookahead to decode
+		// the last real code. In this case we send two empty static blocks instead
+		// of one. (There are no problems if the previous block is stored or fixed.)
+		// To simplify the code, we assume the worst case of last real code encoded
+		// on one bit only.
+		function _tr_align() {
+			send_bits(STATIC_TREES << 1, 3);
+			send_code(END_BLOCK, StaticTree.static_ltree);
+
+			bi_flush();
+
+			// Of the 10 bits for the empty block, we have already sent
+			// (10 - bi_valid) bits. The lookahead for the last real code (before
+			// the EOB of the previous block) was thus at least one plus the length
+			// of the EOB plus what we have just sent of the empty static block.
+			if (1 + last_eob_len + 10 - bi_valid < 9) {
+				send_bits(STATIC_TREES << 1, 3);
+				send_code(END_BLOCK, StaticTree.static_ltree);
+				bi_flush();
+			}
+			last_eob_len = 7;
+		}
+
+		// Save the match info and tally the frequency counts. Return true if
+		// the current block must be flushed.
+		function _tr_tally(dist, // distance of matched string
+		lc // match length-MIN_MATCH or unmatched char (if dist==0)
+		) {
+			var out_length, in_length, dcode;
+			that.pending_buf[d_buf + last_lit * 2] = (dist >>> 8) & 0xff;
+			that.pending_buf[d_buf + last_lit * 2 + 1] = dist & 0xff;
+
+			that.pending_buf[l_buf + last_lit] = lc & 0xff;
+			last_lit++;
+
+			if (dist === 0) {
+				// lc is the unmatched char
+				dyn_ltree[lc * 2]++;
+			} else {
+				matches++;
+				// Here, lc is the match length - MIN_MATCH
+				dist--; // dist = match distance - 1
+				dyn_ltree[(Tree._length_code[lc] + LITERALS + 1) * 2]++;
+				dyn_dtree[Tree.d_code(dist) * 2]++;
+			}
+
+			if ((last_lit & 0x1fff) === 0 && level > 2) {
+				// Compute an upper bound for the compressed length
+				out_length = last_lit * 8;
+				in_length = strstart - block_start;
+				for (dcode = 0; dcode < D_CODES; dcode++) {
+					out_length += dyn_dtree[dcode * 2] * (5 + Tree.extra_dbits[dcode]);
+				}
+				out_length >>>= 3;
+				if ((matches < Math.floor(last_lit / 2)) && out_length < Math.floor(in_length / 2))
+					return true;
+			}
+
+			return (last_lit == lit_bufsize - 1);
+			// We avoid equality with lit_bufsize because of wraparound at 64K
+			// on 16 bit machines and because stored blocks are restricted to
+			// 64K-1 bytes.
+		}
+
+		// Send the block data compressed using the given Huffman trees
+		function compress_block(ltree, dtree) {
+			var dist; // distance of matched string
+			var lc; // match length or unmatched char (if dist === 0)
+			var lx = 0; // running index in l_buf
+			var code; // the code to send
+			var extra; // number of extra bits to send
+
+			if (last_lit !== 0) {
+				do {
+					dist = ((that.pending_buf[d_buf + lx * 2] << 8) & 0xff00) | (that.pending_buf[d_buf + lx * 2 + 1] & 0xff);
+					lc = (that.pending_buf[l_buf + lx]) & 0xff;
+					lx++;
+
+					if (dist === 0) {
+						send_code(lc, ltree); // send a literal byte
+					} else {
+						// Here, lc is the match length - MIN_MATCH
+						code = Tree._length_code[lc];
+
+						send_code(code + LITERALS + 1, ltree); // send the length
+						// code
+						extra = Tree.extra_lbits[code];
+						if (extra !== 0) {
+							lc -= Tree.base_length[code];
+							send_bits(lc, extra); // send the extra length bits
+						}
+						dist--; // dist is now the match distance - 1
+						code = Tree.d_code(dist);
+
+						send_code(code, dtree); // send the distance code
+						extra = Tree.extra_dbits[code];
+						if (extra !== 0) {
+							dist -= Tree.base_dist[code];
+							send_bits(dist, extra); // send the extra distance bits
+						}
+					} // literal or match pair ?
+
+					// Check that the overlay between pending_buf and d_buf+l_buf is
+					// ok:
+				} while (lx < last_lit);
+			}
+
+			send_code(END_BLOCK, ltree);
+			last_eob_len = ltree[END_BLOCK * 2 + 1];
+		}
+
+		// Flush the bit buffer and align the output on a byte boundary
+		function bi_windup() {
+			if (bi_valid > 8) {
+				put_short(bi_buf);
+			} else if (bi_valid > 0) {
+				put_byte(bi_buf & 0xff);
+			}
+			bi_buf = 0;
+			bi_valid = 0;
+		}
+
+		// Copy a stored block, storing first the length and its
+		// one's complement if requested.
+		function copy_block(buf, // the input data
+		len, // its length
+		header // true if block header must be written
+		) {
+			bi_windup(); // align on byte boundary
+			last_eob_len = 8; // enough lookahead for inflate
+
+			if (header) {
+				put_short(len);
+				put_short(~len);
+			}
+
+			that.pending_buf.set(window.subarray(buf, buf + len), that.pending);
+			that.pending += len;
+		}
+
+		// Send a stored block
+		function _tr_stored_block(buf, // input block
+		stored_len, // length of input block
+		eof // true if this is the last block for a file
+		) {
+			send_bits((STORED_BLOCK << 1) + (eof ? 1 : 0), 3); // send block type
+			copy_block(buf, stored_len, true); // with header
+		}
+
+		// Determine the best encoding for the current block: dynamic trees, static
+		// trees or store, and output the encoded block to the zip file.
+		function _tr_flush_block(buf, // input block, or NULL if too old
+		stored_len, // length of input block
+		eof // true if this is the last block for a file
+		) {
+			var opt_lenb, static_lenb;// opt_len and static_len in bytes
+			var max_blindex = 0; // index of last bit length code of non zero freq
+
+			// Build the Huffman trees unless a stored block is forced
+			if (level > 0) {
+				// Construct the literal and distance trees
+				l_desc.build_tree(that);
+
+				d_desc.build_tree(that);
+
+				// At this point, opt_len and static_len are the total bit lengths
+				// of
+				// the compressed block data, excluding the tree representations.
+
+				// Build the bit length tree for the above two trees, and get the
+				// index
+				// in bl_order of the last bit length code to send.
+				max_blindex = build_bl_tree();
+
+				// Determine the best encoding. Compute first the block length in
+				// bytes
+				opt_lenb = (that.opt_len + 3 + 7) >>> 3;
+				static_lenb = (that.static_len + 3 + 7) >>> 3;
+
+				if (static_lenb <= opt_lenb)
+					opt_lenb = static_lenb;
+			} else {
+				opt_lenb = static_lenb = stored_len + 5; // force a stored block
+			}
+
+			if ((stored_len + 4 <= opt_lenb) && buf != -1) {
+				// 4: two words for the lengths
+				// The test buf != NULL is only necessary if LIT_BUFSIZE > WSIZE.
+				// Otherwise we can't have processed more than WSIZE input bytes
+				// since
+				// the last block flush, because compression would have been
+				// successful. If LIT_BUFSIZE <= WSIZE, it is never too late to
+				// transform a block into a stored block.
+				_tr_stored_block(buf, stored_len, eof);
+			} else if (static_lenb == opt_lenb) {
+				send_bits((STATIC_TREES << 1) + (eof ? 1 : 0), 3);
+				compress_block(StaticTree.static_ltree, StaticTree.static_dtree);
+			} else {
+				send_bits((DYN_TREES << 1) + (eof ? 1 : 0), 3);
+				send_all_trees(l_desc.max_code + 1, d_desc.max_code + 1, max_blindex + 1);
+				compress_block(dyn_ltree, dyn_dtree);
+			}
+
+			// The above check is made mod 2^32, for files larger than 512 MB
+			// and uLong implemented on 32 bits.
+
+			init_block();
+
+			if (eof) {
+				bi_windup();
+			}
+		}
+
+		function flush_block_only(eof) {
+			_tr_flush_block(block_start >= 0 ? block_start : -1, strstart - block_start, eof);
+			block_start = strstart;
+			strm.flush_pending();
+		}
+
+		// Fill the window when the lookahead becomes insufficient.
+		// Updates strstart and lookahead.
+		//
+		// IN assertion: lookahead < MIN_LOOKAHEAD
+		// OUT assertions: strstart <= window_size-MIN_LOOKAHEAD
+		// At least one byte has been read, or avail_in === 0; reads are
+		// performed for at least two bytes (required for the zip translate_eol
+		// option -- not supported here).
+		function fill_window() {
+			var n, m;
+			var p;
+			var more; // Amount of free space at the end of the window.
+
+			do {
+				more = (window_size - lookahead - strstart);
+
+				// Deal with !@#$% 64K limit:
+				if (more === 0 && strstart === 0 && lookahead === 0) {
+					more = w_size;
+				} else if (more == -1) {
+					// Very unlikely, but possible on 16 bit machine if strstart ==
+					// 0
+					// and lookahead == 1 (input done one byte at time)
+					more--;
+
+					// If the window is almost full and there is insufficient
+					// lookahead,
+					// move the upper half to the lower one to make room in the
+					// upper half.
+				} else if (strstart >= w_size + w_size - MIN_LOOKAHEAD) {
+					window.set(window.subarray(w_size, w_size + w_size), 0);
+
+					match_start -= w_size;
+					strstart -= w_size; // we now have strstart >= MAX_DIST
+					block_start -= w_size;
+
+					// Slide the hash table (could be avoided with 32 bit values
+					// at the expense of memory usage). We slide even when level ==
+					// 0
+					// to keep the hash table consistent if we switch back to level
+					// > 0
+					// later. (Using level 0 permanently is not an optimal usage of
+					// zlib, so we don't care about this pathological case.)
+
+					n = hash_size;
+					p = n;
+					do {
+						m = (head[--p] & 0xffff);
+						head[p] = (m >= w_size ? m - w_size : 0);
+					} while (--n !== 0);
+
+					n = w_size;
+					p = n;
+					do {
+						m = (prev[--p] & 0xffff);
+						prev[p] = (m >= w_size ? m - w_size : 0);
+						// If n is not on any hash chain, prev[n] is garbage but
+						// its value will never be used.
+					} while (--n !== 0);
+					more += w_size;
+				}
+
+				if (strm.avail_in === 0)
+					return;
+
+				// If there was no sliding:
+				// strstart <= WSIZE+MAX_DIST-1 && lookahead <= MIN_LOOKAHEAD - 1 &&
+				// more == window_size - lookahead - strstart
+				// => more >= window_size - (MIN_LOOKAHEAD-1 + WSIZE + MAX_DIST-1)
+				// => more >= window_size - 2*WSIZE + 2
+				// In the BIG_MEM or MMAP case (not yet supported),
+				// window_size == input_size + MIN_LOOKAHEAD &&
+				// strstart + s->lookahead <= input_size => more >= MIN_LOOKAHEAD.
+				// Otherwise, window_size == 2*WSIZE so more >= 2.
+				// If there was sliding, more >= WSIZE. So in all cases, more >= 2.
+
+				n = strm.read_buf(window, strstart + lookahead, more);
+				lookahead += n;
+
+				// Initialize the hash value now that we have some input:
+				if (lookahead >= MIN_MATCH) {
+					ins_h = window[strstart] & 0xff;
+					ins_h = (((ins_h) << hash_shift) ^ (window[strstart + 1] & 0xff)) & hash_mask;
+				}
+				// If the whole input has less than MIN_MATCH bytes, ins_h is
+				// garbage,
+				// but this is not important since only literal bytes will be
+				// emitted.
+			} while (lookahead < MIN_LOOKAHEAD && strm.avail_in !== 0);
+		}
+
+		// Copy without compression as much as possible from the input stream,
+		// return
+		// the current block state.
+		// This function does not insert new strings in the dictionary since
+		// uncompressible data is probably not useful. This function is used
+		// only for the level=0 compression option.
+		// NOTE: this function should be optimized to avoid extra copying from
+		// window to pending_buf.
+		function deflate_stored(flush) {
+			// Stored blocks are limited to 0xffff bytes, pending_buf is limited
+			// to pending_buf_size, and each stored block has a 5 byte header:
+
+			var max_block_size = 0xffff;
+			var max_start;
+
+			if (max_block_size > pending_buf_size - 5) {
+				max_block_size = pending_buf_size - 5;
+			}
+
+			// Copy as much as possible from input to output:
+			while (true) {
+				// Fill the window as much as possible:
+				if (lookahead <= 1) {
+					fill_window();
+					if (lookahead === 0 && flush == Z_NO_FLUSH)
+						return NeedMore;
+					if (lookahead === 0)
+						break; // flush the current block
+				}
+
+				strstart += lookahead;
+				lookahead = 0;
+
+				// Emit a stored block if pending_buf will be full:
+				max_start = block_start + max_block_size;
+				if (strstart === 0 || strstart >= max_start) {
+					// strstart === 0 is possible when wraparound on 16-bit machine
+					lookahead = (strstart - max_start);
+					strstart = max_start;
+
+					flush_block_only(false);
+					if (strm.avail_out === 0)
+						return NeedMore;
+
+				}
+
+				// Flush if we may have to slide, otherwise block_start may become
+				// negative and the data will be gone:
+				if (strstart - block_start >= w_size - MIN_LOOKAHEAD) {
+					flush_block_only(false);
+					if (strm.avail_out === 0)
+						return NeedMore;
+				}
+			}
+
+			flush_block_only(flush == Z_FINISH);
+			if (strm.avail_out === 0)
+				return (flush == Z_FINISH) ? FinishStarted : NeedMore;
+
+			return flush == Z_FINISH ? FinishDone : BlockDone;
+		}
+
+		function longest_match(cur_match) {
+			var chain_length = max_chain_length; // max hash chain length
+			var scan = strstart; // current string
+			var match; // matched string
+			var len; // length of current match
+			var best_len = prev_length; // best match length so far
+			var limit = strstart > (w_size - MIN_LOOKAHEAD) ? strstart - (w_size - MIN_LOOKAHEAD) : 0;
+			var _nice_match = nice_match;
+
+			// Stop when cur_match becomes <= limit. To simplify the code,
+			// we prevent matches with the string of window index 0.
+
+			var wmask = w_mask;
+
+			var strend = strstart + MAX_MATCH;
+			var scan_end1 = window[scan + best_len - 1];
+			var scan_end = window[scan + best_len];
+
+			// The code is optimized for HASH_BITS >= 8 and MAX_MATCH-2 multiple of
+			// 16.
+			// It is easy to get rid of this optimization if necessary.
+
+			// Do not waste too much time if we already have a good match:
+			if (prev_length >= good_match) {
+				chain_length >>= 2;
+			}
+
+			// Do not look for matches beyond the end of the input. This is
+			// necessary
+			// to make deflate deterministic.
+			if (_nice_match > lookahead)
+				_nice_match = lookahead;
+
+			do {
+				match = cur_match;
+
+				// Skip to next match if the match length cannot increase
+				// or if the match length is less than 2:
+				if (window[match + best_len] != scan_end || window[match + best_len - 1] != scan_end1 || window[match] != window[scan]
+						|| window[++match] != window[scan + 1])
+					continue;
+
+				// The check at best_len-1 can be removed because it will be made
+				// again later. (This heuristic is not always a win.)
+				// It is not necessary to compare scan[2] and match[2] since they
+				// are always equal when the other bytes match, given that
+				// the hash keys are equal and that HASH_BITS >= 8.
+				scan += 2;
+				match++;
+
+				// We check for insufficient lookahead only every 8th comparison;
+				// the 256th check will be made at strstart+258.
+				do {
+				} while (window[++scan] == window[++match] && window[++scan] == window[++match] && window[++scan] == window[++match]
+						&& window[++scan] == window[++match] && window[++scan] == window[++match] && window[++scan] == window[++match]
+						&& window[++scan] == window[++match] && window[++scan] == window[++match] && scan < strend);
+
+				len = MAX_MATCH - (strend - scan);
+				scan = strend - MAX_MATCH;
+
+				if (len > best_len) {
+					match_start = cur_match;
+					best_len = len;
+					if (len >= _nice_match)
+						break;
+					scan_end1 = window[scan + best_len - 1];
+					scan_end = window[scan + best_len];
+				}
+
+			} while ((cur_match = (prev[cur_match & wmask] & 0xffff)) > limit && --chain_length !== 0);
+
+			if (best_len <= lookahead)
+				return best_len;
+			return lookahead;
+		}
+
+		// Compress as much as possible from the input stream, return the current
+		// block state.
+		// This function does not perform lazy evaluation of matches and inserts
+		// new strings in the dictionary only for unmatched strings or for short
+		// matches. It is used only for the fast compression options.
+		function deflate_fast(flush) {
+			// short hash_head = 0; // head of the hash chain
+			var hash_head = 0; // head of the hash chain
+			var bflush; // set if current block must be flushed
+
+			while (true) {
+				// Make sure that we always have enough lookahead, except
+				// at the end of the input file. We need MAX_MATCH bytes
+				// for the next match, plus MIN_MATCH bytes to insert the
+				// string following the next match.
+				if (lookahead < MIN_LOOKAHEAD) {
+					fill_window();
+					if (lookahead < MIN_LOOKAHEAD && flush == Z_NO_FLUSH) {
+						return NeedMore;
+					}
+					if (lookahead === 0)
+						break; // flush the current block
+				}
+
+				// Insert the string window[strstart .. strstart+2] in the
+				// dictionary, and set hash_head to the head of the hash chain:
+				if (lookahead >= MIN_MATCH) {
+					ins_h = (((ins_h) << hash_shift) ^ (window[(strstart) + (MIN_MATCH - 1)] & 0xff)) & hash_mask;
+
+					// prev[strstart&w_mask]=hash_head=head[ins_h];
+					hash_head = (head[ins_h] & 0xffff);
+					prev[strstart & w_mask] = head[ins_h];
+					head[ins_h] = strstart;
+				}
+
+				// Find the longest match, discarding those <= prev_length.
+				// At this point we have always match_length < MIN_MATCH
+
+				if (hash_head !== 0 && ((strstart - hash_head) & 0xffff) <= w_size - MIN_LOOKAHEAD) {
+					// To simplify the code, we prevent matches with the string
+					// of window index 0 (in particular we have to avoid a match
+					// of the string with itself at the start of the input file).
+					if (strategy != Z_HUFFMAN_ONLY) {
+						match_length = longest_match(hash_head);
+					}
+					// longest_match() sets match_start
+				}
+				if (match_length >= MIN_MATCH) {
+					// check_match(strstart, match_start, match_length);
+
+					bflush = _tr_tally(strstart - match_start, match_length - MIN_MATCH);
+
+					lookahead -= match_length;
+
+					// Insert new strings in the hash table only if the match length
+					// is not too large. This saves time but degrades compression.
+					if (match_length <= max_lazy_match && lookahead >= MIN_MATCH) {
+						match_length--; // string at strstart already in hash table
+						do {
+							strstart++;
+
+							ins_h = ((ins_h << hash_shift) ^ (window[(strstart) + (MIN_MATCH - 1)] & 0xff)) & hash_mask;
+							// prev[strstart&w_mask]=hash_head=head[ins_h];
+							hash_head = (head[ins_h] & 0xffff);
+							prev[strstart & w_mask] = head[ins_h];
+							head[ins_h] = strstart;
+
+							// strstart never exceeds WSIZE-MAX_MATCH, so there are
+							// always MIN_MATCH bytes ahead.
+						} while (--match_length !== 0);
+						strstart++;
+					} else {
+						strstart += match_length;
+						match_length = 0;
+						ins_h = window[strstart] & 0xff;
+
+						ins_h = (((ins_h) << hash_shift) ^ (window[strstart + 1] & 0xff)) & hash_mask;
+						// If lookahead < MIN_MATCH, ins_h is garbage, but it does
+						// not
+						// matter since it will be recomputed at next deflate call.
+					}
+				} else {
+					// No match, output a literal byte
+
+					bflush = _tr_tally(0, window[strstart] & 0xff);
+					lookahead--;
+					strstart++;
+				}
+				if (bflush) {
+
+					flush_block_only(false);
+					if (strm.avail_out === 0)
+						return NeedMore;
+				}
+			}
+
+			flush_block_only(flush == Z_FINISH);
+			if (strm.avail_out === 0) {
+				if (flush == Z_FINISH)
+					return FinishStarted;
+				else
+					return NeedMore;
+			}
+			return flush == Z_FINISH ? FinishDone : BlockDone;
+		}
+
+		// Same as above, but achieves better compression. We use a lazy
+		// evaluation for matches: a match is finally adopted only if there is
+		// no better match at the next window position.
+		function deflate_slow(flush) {
+			// short hash_head = 0; // head of hash chain
+			var hash_head = 0; // head of hash chain
+			var bflush; // set if current block must be flushed
+			var max_insert;
+
+			// Process the input block.
+			while (true) {
+				// Make sure that we always have enough lookahead, except
+				// at the end of the input file. We need MAX_MATCH bytes
+				// for the next match, plus MIN_MATCH bytes to insert the
+				// string following the next match.
+
+				if (lookahead < MIN_LOOKAHEAD) {
+					fill_window();
+					if (lookahead < MIN_LOOKAHEAD && flush == Z_NO_FLUSH) {
+						return NeedMore;
+					}
+					if (lookahead === 0)
+						break; // flush the current block
+				}
+
+				// Insert the string window[strstart .. strstart+2] in the
+				// dictionary, and set hash_head to the head of the hash chain:
+
+				if (lookahead >= MIN_MATCH) {
+					ins_h = (((ins_h) << hash_shift) ^ (window[(strstart) + (MIN_MATCH - 1)] & 0xff)) & hash_mask;
+					// prev[strstart&w_mask]=hash_head=head[ins_h];
+					hash_head = (head[ins_h] & 0xffff);
+					prev[strstart & w_mask] = head[ins_h];
+					head[ins_h] = strstart;
+				}
+
+				// Find the longest match, discarding those <= prev_length.
+				prev_length = match_length;
+				prev_match = match_start;
+				match_length = MIN_MATCH - 1;
+
+				if (hash_head !== 0 && prev_length < max_lazy_match && ((strstart - hash_head) & 0xffff) <= w_size - MIN_LOOKAHEAD) {
+					// To simplify the code, we prevent matches with the string
+					// of window index 0 (in particular we have to avoid a match
+					// of the string with itself at the start of the input file).
+
+					if (strategy != Z_HUFFMAN_ONLY) {
+						match_length = longest_match(hash_head);
+					}
+					// longest_match() sets match_start
+
+					if (match_length <= 5 && (strategy == Z_FILTERED || (match_length == MIN_MATCH && strstart - match_start > 4096))) {
+
+						// If prev_match is also MIN_MATCH, match_start is garbage
+						// but we will ignore the current match anyway.
+						match_length = MIN_MATCH - 1;
+					}
+				}
+
+				// If there was a match at the previous step and the current
+				// match is not better, output the previous match:
+				if (prev_length >= MIN_MATCH && match_length <= prev_length) {
+					max_insert = strstart + lookahead - MIN_MATCH;
+					// Do not insert strings in hash table beyond this.
+
+					// check_match(strstart-1, prev_match, prev_length);
+
+					bflush = _tr_tally(strstart - 1 - prev_match, prev_length - MIN_MATCH);
+
+					// Insert in hash table all strings up to the end of the match.
+					// strstart-1 and strstart are already inserted. If there is not
+					// enough lookahead, the last two strings are not inserted in
+					// the hash table.
+					lookahead -= prev_length - 1;
+					prev_length -= 2;
+					do {
+						if (++strstart <= max_insert) {
+							ins_h = (((ins_h) << hash_shift) ^ (window[(strstart) + (MIN_MATCH - 1)] & 0xff)) & hash_mask;
+							// prev[strstart&w_mask]=hash_head=head[ins_h];
+							hash_head = (head[ins_h] & 0xffff);
+							prev[strstart & w_mask] = head[ins_h];
+							head[ins_h] = strstart;
+						}
+					} while (--prev_length !== 0);
+					match_available = 0;
+					match_length = MIN_MATCH - 1;
+					strstart++;
+
+					if (bflush) {
+						flush_block_only(false);
+						if (strm.avail_out === 0)
+							return NeedMore;
+					}
+				} else if (match_available !== 0) {
+
+					// If there was no match at the previous position, output a
+					// single literal. If there was a match but the current match
+					// is longer, truncate the previous match to a single literal.
+
+					bflush = _tr_tally(0, window[strstart - 1] & 0xff);
+
+					if (bflush) {
+						flush_block_only(false);
+					}
+					strstart++;
+					lookahead--;
+					if (strm.avail_out === 0)
+						return NeedMore;
+				} else {
+					// There is no previous match to compare with, wait for
+					// the next step to decide.
+
+					match_available = 1;
+					strstart++;
+					lookahead--;
+				}
+			}
+
+			if (match_available !== 0) {
+				bflush = _tr_tally(0, window[strstart - 1] & 0xff);
+				match_available = 0;
+			}
+			flush_block_only(flush == Z_FINISH);
+
+			if (strm.avail_out === 0) {
+				if (flush == Z_FINISH)
+					return FinishStarted;
+				else
+					return NeedMore;
+			}
+
+			return flush == Z_FINISH ? FinishDone : BlockDone;
+		}
+
+		function deflateReset(strm) {
+			strm.total_in = strm.total_out = 0;
+			strm.msg = null; //
+			
+			that.pending = 0;
+			that.pending_out = 0;
+
+			status = BUSY_STATE;
+
+			last_flush = Z_NO_FLUSH;
+
+			tr_init();
+			lm_init();
+			return Z_OK;
+		}
+
+		that.deflateInit = function(strm, _level, bits, _method, memLevel, _strategy) {
+			if (!_method)
+				_method = Z_DEFLATED;
+			if (!memLevel)
+				memLevel = DEF_MEM_LEVEL;
+			if (!_strategy)
+				_strategy = Z_DEFAULT_STRATEGY;
+
+			// byte[] my_version=ZLIB_VERSION;
+
+			//
+			// if (!version || version[0] != my_version[0]
+			// || stream_size != sizeof(z_stream)) {
+			// return Z_VERSION_ERROR;
+			// }
+
+			strm.msg = null;
+
+			if (_level == Z_DEFAULT_COMPRESSION)
+				_level = 6;
+
+			if (memLevel < 1 || memLevel > MAX_MEM_LEVEL || _method != Z_DEFLATED || bits < 9 || bits > 15 || _level < 0 || _level > 9 || _strategy < 0
+					|| _strategy > Z_HUFFMAN_ONLY) {
+				return Z_STREAM_ERROR;
+			}
+
+			strm.dstate = that;
+
+			w_bits = bits;
+			w_size = 1 << w_bits;
+			w_mask = w_size - 1;
+
+			hash_bits = memLevel + 7;
+			hash_size = 1 << hash_bits;
+			hash_mask = hash_size - 1;
+			hash_shift = Math.floor((hash_bits + MIN_MATCH - 1) / MIN_MATCH);
+
+			window = new Uint8Array(w_size * 2);
+			prev = [];
+			head = [];
+
+			lit_bufsize = 1 << (memLevel + 6); // 16K elements by default
+
+			// We overlay pending_buf and d_buf+l_buf. This works since the average
+			// output size for (length,distance) codes is <= 24 bits.
+			that.pending_buf = new Uint8Array(lit_bufsize * 4);
+			pending_buf_size = lit_bufsize * 4;
+
+			d_buf = Math.floor(lit_bufsize / 2);
+			l_buf = (1 + 2) * lit_bufsize;
+
+			level = _level;
+
+			strategy = _strategy;
+			method = _method & 0xff;
+
+			return deflateReset(strm);
+		};
+
+		that.deflateEnd = function() {
+			if (status != INIT_STATE && status != BUSY_STATE && status != FINISH_STATE) {
+				return Z_STREAM_ERROR;
+			}
+			// Deallocate in reverse order of allocations:
+			that.pending_buf = null;
+			head = null;
+			prev = null;
+			window = null;
+			// free
+			that.dstate = null;
+			return status == BUSY_STATE ? Z_DATA_ERROR : Z_OK;
+		};
+
+		that.deflateParams = function(strm, _level, _strategy) {
+			var err = Z_OK;
+
+			if (_level == Z_DEFAULT_COMPRESSION) {
+				_level = 6;
+			}
+			if (_level < 0 || _level > 9 || _strategy < 0 || _strategy > Z_HUFFMAN_ONLY) {
+				return Z_STREAM_ERROR;
+			}
+
+			if (config_table[level].func != config_table[_level].func && strm.total_in !== 0) {
+				// Flush the last buffer:
+				err = strm.deflate(Z_PARTIAL_FLUSH);
+			}
+
+			if (level != _level) {
+				level = _level;
+				max_lazy_match = config_table[level].max_lazy;
+				good_match = config_table[level].good_length;
+				nice_match = config_table[level].nice_length;
+				max_chain_length = config_table[level].max_chain;
+			}
+			strategy = _strategy;
+			return err;
+		};
+
+		that.deflateSetDictionary = function(strm, dictionary, dictLength) {
+			var length = dictLength;
+			var n, index = 0;
+
+			if (!dictionary || status != INIT_STATE)
+				return Z_STREAM_ERROR;
+
+			if (length < MIN_MATCH)
+				return Z_OK;
+			if (length > w_size - MIN_LOOKAHEAD) {
+				length = w_size - MIN_LOOKAHEAD;
+				index = dictLength - length; // use the tail of the dictionary
+			}
+			window.set(dictionary.subarray(index, index + length), 0);
+
+			strstart = length;
+			block_start = length;
+
+			// Insert all strings in the hash table (except for the last two bytes).
+			// s->lookahead stays null, so s->ins_h will be recomputed at the next
+			// call of fill_window.
+
+			ins_h = window[0] & 0xff;
+			ins_h = (((ins_h) << hash_shift) ^ (window[1] & 0xff)) & hash_mask;
+
+			for (n = 0; n <= length - MIN_MATCH; n++) {
+				ins_h = (((ins_h) << hash_shift) ^ (window[(n) + (MIN_MATCH - 1)] & 0xff)) & hash_mask;
+				prev[n & w_mask] = head[ins_h];
+				head[ins_h] = n;
+			}
+			return Z_OK;
+		};
+
+		that.deflate = function(_strm, flush) {
+			var i, header, level_flags, old_flush, bstate;
+
+			if (flush > Z_FINISH || flush < 0) {
+				return Z_STREAM_ERROR;
+			}
+
+			if (!_strm.next_out || (!_strm.next_in && _strm.avail_in !== 0) || (status == FINISH_STATE && flush != Z_FINISH)) {
+				_strm.msg = z_errmsg[Z_NEED_DICT - (Z_STREAM_ERROR)];
+				return Z_STREAM_ERROR;
+			}
+			if (_strm.avail_out === 0) {
+				_strm.msg = z_errmsg[Z_NEED_DICT - (Z_BUF_ERROR)];
+				return Z_BUF_ERROR;
+			}
+
+			strm = _strm; // just in case
+			old_flush = last_flush;
+			last_flush = flush;
+
+			// Write the zlib header
+			if (status == INIT_STATE) {
+				header = (Z_DEFLATED + ((w_bits - 8) << 4)) << 8;
+				level_flags = ((level - 1) & 0xff) >> 1;
+
+				if (level_flags > 3)
+					level_flags = 3;
+				header |= (level_flags << 6);
+				if (strstart !== 0)
+					header |= PRESET_DICT;
+				header += 31 - (header % 31);
+
+				status = BUSY_STATE;
+				putShortMSB(header);
+			}
+
+			// Flush as much pending output as possible
+			if (that.pending !== 0) {
+				strm.flush_pending();
+				if (strm.avail_out === 0) {
+					// console.log(" avail_out==0");
+					// Since avail_out is 0, deflate will be called again with
+					// more output space, but possibly with both pending and
+					// avail_in equal to zero. There won't be anything to do,
+					// but this is not an error situation so make sure we
+					// return OK instead of BUF_ERROR at next call of deflate:
+					last_flush = -1;
+					return Z_OK;
+				}
+
+				// Make sure there is something to do and avoid duplicate
+				// consecutive
+				// flushes. For repeated and useless calls with Z_FINISH, we keep
+				// returning Z_STREAM_END instead of Z_BUFF_ERROR.
+			} else if (strm.avail_in === 0 && flush <= old_flush && flush != Z_FINISH) {
+				strm.msg = z_errmsg[Z_NEED_DICT - (Z_BUF_ERROR)];
+				return Z_BUF_ERROR;
+			}
+
+			// User must not provide more input after the first FINISH:
+			if (status == FINISH_STATE && strm.avail_in !== 0) {
+				_strm.msg = z_errmsg[Z_NEED_DICT - (Z_BUF_ERROR)];
+				return Z_BUF_ERROR;
+			}
+
+			// Start a new block or continue the current one.
+			if (strm.avail_in !== 0 || lookahead !== 0 || (flush != Z_NO_FLUSH && status != FINISH_STATE)) {
+				bstate = -1;
+				switch (config_table[level].func) {
+				case STORED:
+					bstate = deflate_stored(flush);
+					break;
+				case FAST:
+					bstate = deflate_fast(flush);
+					break;
+				case SLOW:
+					bstate = deflate_slow(flush);
+					break;
+				default:
+				}
+
+				if (bstate == FinishStarted || bstate == FinishDone) {
+					status = FINISH_STATE;
+				}
+				if (bstate == NeedMore || bstate == FinishStarted) {
+					if (strm.avail_out === 0) {
+						last_flush = -1; // avoid BUF_ERROR next call, see above
+					}
+					return Z_OK;
+					// If flush != Z_NO_FLUSH && avail_out === 0, the next call
+					// of deflate should use the same flush parameter to make sure
+					// that the flush is complete. So we don't have to output an
+					// empty block here, this will be done at next call. This also
+					// ensures that for a very small output buffer, we emit at most
+					// one empty block.
+				}
+
+				if (bstate == BlockDone) {
+					if (flush == Z_PARTIAL_FLUSH) {
+						_tr_align();
+					} else { // FULL_FLUSH or SYNC_FLUSH
+						_tr_stored_block(0, 0, false);
+						// For a full flush, this empty block will be recognized
+						// as a special marker by inflate_sync().
+						if (flush == Z_FULL_FLUSH) {
+							// state.head[s.hash_size-1]=0;
+							for (i = 0; i < hash_size/*-1*/; i++)
+								// forget history
+								head[i] = 0;
+						}
+					}
+					strm.flush_pending();
+					if (strm.avail_out === 0) {
+						last_flush = -1; // avoid BUF_ERROR at next call, see above
+						return Z_OK;
+					}
+				}
+			}
+
+			if (flush != Z_FINISH)
+				return Z_OK;
+			return Z_STREAM_END;
+		};
+	}
+
+	// ZStream
+
+	function ZStream() {
+		var that = this;
+		that.next_in_index = 0;
+		that.next_out_index = 0;
+		// that.next_in; // next input byte
+		that.avail_in = 0; // number of bytes available at next_in
+		that.total_in = 0; // total nb of input bytes read so far
+		// that.next_out; // next output byte should be put there
+		that.avail_out = 0; // remaining free space at next_out
+		that.total_out = 0; // total nb of bytes output so far
+		// that.msg;
+		// that.dstate;
+	}
+
+	ZStream.prototype = {
+		deflateInit : function(level, bits) {
+			var that = this;
+			that.dstate = new Deflate();
+			if (!bits)
+				bits = MAX_BITS;
+			return that.dstate.deflateInit(that, level, bits);
+		},
+
+		deflate : function(flush) {
+			var that = this;
+			if (!that.dstate) {
+				return Z_STREAM_ERROR;
+			}
+			return that.dstate.deflate(that, flush);
+		},
+
+		deflateEnd : function() {
+			var that = this;
+			if (!that.dstate)
+				return Z_STREAM_ERROR;
+			var ret = that.dstate.deflateEnd();
+			that.dstate = null;
+			return ret;
+		},
+
+		deflateParams : function(level, strategy) {
+			var that = this;
+			if (!that.dstate)
+				return Z_STREAM_ERROR;
+			return that.dstate.deflateParams(that, level, strategy);
+		},
+
+		deflateSetDictionary : function(dictionary, dictLength) {
+			var that = this;
+			if (!that.dstate)
+				return Z_STREAM_ERROR;
+			return that.dstate.deflateSetDictionary(that, dictionary, dictLength);
+		},
+
+		// Read a new buffer from the current input stream, update the
+		// total number of bytes read. All deflate() input goes through
+		// this function so some applications may wish to modify it to avoid
+		// allocating a large strm->next_in buffer and copying from it.
+		// (See also flush_pending()).
+		read_buf : function(buf, start, size) {
+			var that = this;
+			var len = that.avail_in;
+			if (len > size)
+				len = size;
+			if (len === 0)
+				return 0;
+			that.avail_in -= len;
+			buf.set(that.next_in.subarray(that.next_in_index, that.next_in_index + len), start);
+			that.next_in_index += len;
+			that.total_in += len;
+			return len;
+		},
+
+		// Flush as much pending output as possible. All deflate() output goes
+		// through this function so some applications may wish to modify it
+		// to avoid allocating a large strm->next_out buffer and copying into it.
+		// (See also read_buf()).
+		flush_pending : function() {
+			var that = this;
+			var len = that.dstate.pending;
+
+			if (len > that.avail_out)
+				len = that.avail_out;
+			if (len === 0)
+				return;
+
+			// if (that.dstate.pending_buf.length <= that.dstate.pending_out || that.next_out.length <= that.next_out_index
+			// || that.dstate.pending_buf.length < (that.dstate.pending_out + len) || that.next_out.length < (that.next_out_index +
+			// len)) {
+			// console.log(that.dstate.pending_buf.length + ", " + that.dstate.pending_out + ", " + that.next_out.length + ", " +
+			// that.next_out_index + ", " + len);
+			// console.log("avail_out=" + that.avail_out);
+			// }
+
+			that.next_out.set(that.dstate.pending_buf.subarray(that.dstate.pending_out, that.dstate.pending_out + len), that.next_out_index);
+
+			that.next_out_index += len;
+			that.dstate.pending_out += len;
+			that.total_out += len;
+			that.avail_out -= len;
+			that.dstate.pending -= len;
+			if (that.dstate.pending === 0) {
+				that.dstate.pending_out = 0;
+			}
+		}
+	};
+
+	// Deflater
+
+	function Deflater(options) {
+		var that = this;
+		var z = new ZStream();
+		var bufsize = 512;
+		var flush = Z_NO_FLUSH;
+		var buf = new Uint8Array(bufsize);
+		var level = options ? options.level : Z_DEFAULT_COMPRESSION;
+		if (typeof level == "undefined")
+			level = Z_DEFAULT_COMPRESSION;
+		z.deflateInit(level);
+		z.next_out = buf;
+
+		that.append = function(data, onprogress) {
+			var err, buffers = [], lastIndex = 0, bufferIndex = 0, bufferSize = 0, array;
+			if (!data.length)
+				return;
+			z.next_in_index = 0;
+			z.next_in = data;
+			z.avail_in = data.length;
+			do {
+				z.next_out_index = 0;
+				z.avail_out = bufsize;
+				err = z.deflate(flush);
+				if (err != Z_OK)
+					throw new Error("deflating: " + z.msg);
+				if (z.next_out_index)
+					if (z.next_out_index == bufsize)
+						buffers.push(new Uint8Array(buf));
+					else
+						buffers.push(new Uint8Array(buf.subarray(0, z.next_out_index)));
+				bufferSize += z.next_out_index;
+				if (onprogress && z.next_in_index > 0 && z.next_in_index != lastIndex) {
+					onprogress(z.next_in_index);
+					lastIndex = z.next_in_index;
+				}
+			} while (z.avail_in > 0 || z.avail_out === 0);
+			array = new Uint8Array(bufferSize);
+			buffers.forEach(function(chunk) {
+				array.set(chunk, bufferIndex);
+				bufferIndex += chunk.length;
+			});
+			return array;
+		};
+		that.flush = function() {
+			var err, buffers = [], bufferIndex = 0, bufferSize = 0, array;
+			do {
+				z.next_out_index = 0;
+				z.avail_out = bufsize;
+				err = z.deflate(Z_FINISH);
+				if (err != Z_STREAM_END && err != Z_OK)
+					throw new Error("deflating: " + z.msg);
+				if (bufsize - z.avail_out > 0)
+					buffers.push(new Uint8Array(buf.subarray(0, z.next_out_index)));
+				bufferSize += z.next_out_index;
+			} while (z.avail_in > 0 || z.avail_out === 0);
+			z.deflateEnd();
+			array = new Uint8Array(bufferSize);
+			buffers.forEach(function(chunk) {
+				array.set(chunk, bufferIndex);
+				bufferIndex += chunk.length;
+			});
+			return array;
+		};
+	}
+
+	// 'zip' may not be defined in z-worker and some tests
+	var env = global.zip || global;
+	env.Deflater = env._jzlib_Deflater = Deflater;
+})(this);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mda_heatmap_viz/static/javascript/lib/inflate.js	Thu Apr 07 14:43:37 2016 -0400
@@ -0,0 +1,2155 @@
+/*
+ Copyright (c) 2013 Gildas Lormeau. All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+
+ 1. Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above copyright 
+ notice, this list of conditions and the following disclaimer in 
+ the documentation and/or other materials provided with the distribution.
+
+ 3. The names of the authors may not be used to endorse or promote products
+ derived from this software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
+ INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+ FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT,
+ INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT,
+ INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
+ OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+ EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/*
+ * This program is based on JZlib 1.0.2 ymnk, JCraft,Inc.
+ * JZlib is based on zlib-1.1.3, so all credit should go authors
+ * Jean-loup Gailly(jloup@gzip.org) and Mark Adler(madler@alumni.caltech.edu)
+ * and contributors of zlib.
+ */
+
+(function(global) {
+	"use strict";
+
+	// Global
+	var MAX_BITS = 15;
+
+	var Z_OK = 0;
+	var Z_STREAM_END = 1;
+	var Z_NEED_DICT = 2;
+	var Z_STREAM_ERROR = -2;
+	var Z_DATA_ERROR = -3;
+	var Z_MEM_ERROR = -4;
+	var Z_BUF_ERROR = -5;
+
+	var inflate_mask = [ 0x00000000, 0x00000001, 0x00000003, 0x00000007, 0x0000000f, 0x0000001f, 0x0000003f, 0x0000007f, 0x000000ff, 0x000001ff, 0x000003ff,
+			0x000007ff, 0x00000fff, 0x00001fff, 0x00003fff, 0x00007fff, 0x0000ffff ];
+
+	var MANY = 1440;
+
+	// JZlib version : "1.0.2"
+	var Z_NO_FLUSH = 0;
+	var Z_FINISH = 4;
+
+	// InfTree
+	var fixed_bl = 9;
+	var fixed_bd = 5;
+
+	var fixed_tl = [ 96, 7, 256, 0, 8, 80, 0, 8, 16, 84, 8, 115, 82, 7, 31, 0, 8, 112, 0, 8, 48, 0, 9, 192, 80, 7, 10, 0, 8, 96, 0, 8, 32, 0, 9, 160, 0, 8, 0,
+			0, 8, 128, 0, 8, 64, 0, 9, 224, 80, 7, 6, 0, 8, 88, 0, 8, 24, 0, 9, 144, 83, 7, 59, 0, 8, 120, 0, 8, 56, 0, 9, 208, 81, 7, 17, 0, 8, 104, 0, 8, 40,
+			0, 9, 176, 0, 8, 8, 0, 8, 136, 0, 8, 72, 0, 9, 240, 80, 7, 4, 0, 8, 84, 0, 8, 20, 85, 8, 227, 83, 7, 43, 0, 8, 116, 0, 8, 52, 0, 9, 200, 81, 7, 13,
+			0, 8, 100, 0, 8, 36, 0, 9, 168, 0, 8, 4, 0, 8, 132, 0, 8, 68, 0, 9, 232, 80, 7, 8, 0, 8, 92, 0, 8, 28, 0, 9, 152, 84, 7, 83, 0, 8, 124, 0, 8, 60,
+			0, 9, 216, 82, 7, 23, 0, 8, 108, 0, 8, 44, 0, 9, 184, 0, 8, 12, 0, 8, 140, 0, 8, 76, 0, 9, 248, 80, 7, 3, 0, 8, 82, 0, 8, 18, 85, 8, 163, 83, 7,
+			35, 0, 8, 114, 0, 8, 50, 0, 9, 196, 81, 7, 11, 0, 8, 98, 0, 8, 34, 0, 9, 164, 0, 8, 2, 0, 8, 130, 0, 8, 66, 0, 9, 228, 80, 7, 7, 0, 8, 90, 0, 8,
+			26, 0, 9, 148, 84, 7, 67, 0, 8, 122, 0, 8, 58, 0, 9, 212, 82, 7, 19, 0, 8, 106, 0, 8, 42, 0, 9, 180, 0, 8, 10, 0, 8, 138, 0, 8, 74, 0, 9, 244, 80,
+			7, 5, 0, 8, 86, 0, 8, 22, 192, 8, 0, 83, 7, 51, 0, 8, 118, 0, 8, 54, 0, 9, 204, 81, 7, 15, 0, 8, 102, 0, 8, 38, 0, 9, 172, 0, 8, 6, 0, 8, 134, 0,
+			8, 70, 0, 9, 236, 80, 7, 9, 0, 8, 94, 0, 8, 30, 0, 9, 156, 84, 7, 99, 0, 8, 126, 0, 8, 62, 0, 9, 220, 82, 7, 27, 0, 8, 110, 0, 8, 46, 0, 9, 188, 0,
+			8, 14, 0, 8, 142, 0, 8, 78, 0, 9, 252, 96, 7, 256, 0, 8, 81, 0, 8, 17, 85, 8, 131, 82, 7, 31, 0, 8, 113, 0, 8, 49, 0, 9, 194, 80, 7, 10, 0, 8, 97,
+			0, 8, 33, 0, 9, 162, 0, 8, 1, 0, 8, 129, 0, 8, 65, 0, 9, 226, 80, 7, 6, 0, 8, 89, 0, 8, 25, 0, 9, 146, 83, 7, 59, 0, 8, 121, 0, 8, 57, 0, 9, 210,
+			81, 7, 17, 0, 8, 105, 0, 8, 41, 0, 9, 178, 0, 8, 9, 0, 8, 137, 0, 8, 73, 0, 9, 242, 80, 7, 4, 0, 8, 85, 0, 8, 21, 80, 8, 258, 83, 7, 43, 0, 8, 117,
+			0, 8, 53, 0, 9, 202, 81, 7, 13, 0, 8, 101, 0, 8, 37, 0, 9, 170, 0, 8, 5, 0, 8, 133, 0, 8, 69, 0, 9, 234, 80, 7, 8, 0, 8, 93, 0, 8, 29, 0, 9, 154,
+			84, 7, 83, 0, 8, 125, 0, 8, 61, 0, 9, 218, 82, 7, 23, 0, 8, 109, 0, 8, 45, 0, 9, 186, 0, 8, 13, 0, 8, 141, 0, 8, 77, 0, 9, 250, 80, 7, 3, 0, 8, 83,
+			0, 8, 19, 85, 8, 195, 83, 7, 35, 0, 8, 115, 0, 8, 51, 0, 9, 198, 81, 7, 11, 0, 8, 99, 0, 8, 35, 0, 9, 166, 0, 8, 3, 0, 8, 131, 0, 8, 67, 0, 9, 230,
+			80, 7, 7, 0, 8, 91, 0, 8, 27, 0, 9, 150, 84, 7, 67, 0, 8, 123, 0, 8, 59, 0, 9, 214, 82, 7, 19, 0, 8, 107, 0, 8, 43, 0, 9, 182, 0, 8, 11, 0, 8, 139,
+			0, 8, 75, 0, 9, 246, 80, 7, 5, 0, 8, 87, 0, 8, 23, 192, 8, 0, 83, 7, 51, 0, 8, 119, 0, 8, 55, 0, 9, 206, 81, 7, 15, 0, 8, 103, 0, 8, 39, 0, 9, 174,
+			0, 8, 7, 0, 8, 135, 0, 8, 71, 0, 9, 238, 80, 7, 9, 0, 8, 95, 0, 8, 31, 0, 9, 158, 84, 7, 99, 0, 8, 127, 0, 8, 63, 0, 9, 222, 82, 7, 27, 0, 8, 111,
+			0, 8, 47, 0, 9, 190, 0, 8, 15, 0, 8, 143, 0, 8, 79, 0, 9, 254, 96, 7, 256, 0, 8, 80, 0, 8, 16, 84, 8, 115, 82, 7, 31, 0, 8, 112, 0, 8, 48, 0, 9,
+			193, 80, 7, 10, 0, 8, 96, 0, 8, 32, 0, 9, 161, 0, 8, 0, 0, 8, 128, 0, 8, 64, 0, 9, 225, 80, 7, 6, 0, 8, 88, 0, 8, 24, 0, 9, 145, 83, 7, 59, 0, 8,
+			120, 0, 8, 56, 0, 9, 209, 81, 7, 17, 0, 8, 104, 0, 8, 40, 0, 9, 177, 0, 8, 8, 0, 8, 136, 0, 8, 72, 0, 9, 241, 80, 7, 4, 0, 8, 84, 0, 8, 20, 85, 8,
+			227, 83, 7, 43, 0, 8, 116, 0, 8, 52, 0, 9, 201, 81, 7, 13, 0, 8, 100, 0, 8, 36, 0, 9, 169, 0, 8, 4, 0, 8, 132, 0, 8, 68, 0, 9, 233, 80, 7, 8, 0, 8,
+			92, 0, 8, 28, 0, 9, 153, 84, 7, 83, 0, 8, 124, 0, 8, 60, 0, 9, 217, 82, 7, 23, 0, 8, 108, 0, 8, 44, 0, 9, 185, 0, 8, 12, 0, 8, 140, 0, 8, 76, 0, 9,
+			249, 80, 7, 3, 0, 8, 82, 0, 8, 18, 85, 8, 163, 83, 7, 35, 0, 8, 114, 0, 8, 50, 0, 9, 197, 81, 7, 11, 0, 8, 98, 0, 8, 34, 0, 9, 165, 0, 8, 2, 0, 8,
+			130, 0, 8, 66, 0, 9, 229, 80, 7, 7, 0, 8, 90, 0, 8, 26, 0, 9, 149, 84, 7, 67, 0, 8, 122, 0, 8, 58, 0, 9, 213, 82, 7, 19, 0, 8, 106, 0, 8, 42, 0, 9,
+			181, 0, 8, 10, 0, 8, 138, 0, 8, 74, 0, 9, 245, 80, 7, 5, 0, 8, 86, 0, 8, 22, 192, 8, 0, 83, 7, 51, 0, 8, 118, 0, 8, 54, 0, 9, 205, 81, 7, 15, 0, 8,
+			102, 0, 8, 38, 0, 9, 173, 0, 8, 6, 0, 8, 134, 0, 8, 70, 0, 9, 237, 80, 7, 9, 0, 8, 94, 0, 8, 30, 0, 9, 157, 84, 7, 99, 0, 8, 126, 0, 8, 62, 0, 9,
+			221, 82, 7, 27, 0, 8, 110, 0, 8, 46, 0, 9, 189, 0, 8, 14, 0, 8, 142, 0, 8, 78, 0, 9, 253, 96, 7, 256, 0, 8, 81, 0, 8, 17, 85, 8, 131, 82, 7, 31, 0,
+			8, 113, 0, 8, 49, 0, 9, 195, 80, 7, 10, 0, 8, 97, 0, 8, 33, 0, 9, 163, 0, 8, 1, 0, 8, 129, 0, 8, 65, 0, 9, 227, 80, 7, 6, 0, 8, 89, 0, 8, 25, 0, 9,
+			147, 83, 7, 59, 0, 8, 121, 0, 8, 57, 0, 9, 211, 81, 7, 17, 0, 8, 105, 0, 8, 41, 0, 9, 179, 0, 8, 9, 0, 8, 137, 0, 8, 73, 0, 9, 243, 80, 7, 4, 0, 8,
+			85, 0, 8, 21, 80, 8, 258, 83, 7, 43, 0, 8, 117, 0, 8, 53, 0, 9, 203, 81, 7, 13, 0, 8, 101, 0, 8, 37, 0, 9, 171, 0, 8, 5, 0, 8, 133, 0, 8, 69, 0, 9,
+			235, 80, 7, 8, 0, 8, 93, 0, 8, 29, 0, 9, 155, 84, 7, 83, 0, 8, 125, 0, 8, 61, 0, 9, 219, 82, 7, 23, 0, 8, 109, 0, 8, 45, 0, 9, 187, 0, 8, 13, 0, 8,
+			141, 0, 8, 77, 0, 9, 251, 80, 7, 3, 0, 8, 83, 0, 8, 19, 85, 8, 195, 83, 7, 35, 0, 8, 115, 0, 8, 51, 0, 9, 199, 81, 7, 11, 0, 8, 99, 0, 8, 35, 0, 9,
+			167, 0, 8, 3, 0, 8, 131, 0, 8, 67, 0, 9, 231, 80, 7, 7, 0, 8, 91, 0, 8, 27, 0, 9, 151, 84, 7, 67, 0, 8, 123, 0, 8, 59, 0, 9, 215, 82, 7, 19, 0, 8,
+			107, 0, 8, 43, 0, 9, 183, 0, 8, 11, 0, 8, 139, 0, 8, 75, 0, 9, 247, 80, 7, 5, 0, 8, 87, 0, 8, 23, 192, 8, 0, 83, 7, 51, 0, 8, 119, 0, 8, 55, 0, 9,
+			207, 81, 7, 15, 0, 8, 103, 0, 8, 39, 0, 9, 175, 0, 8, 7, 0, 8, 135, 0, 8, 71, 0, 9, 239, 80, 7, 9, 0, 8, 95, 0, 8, 31, 0, 9, 159, 84, 7, 99, 0, 8,
+			127, 0, 8, 63, 0, 9, 223, 82, 7, 27, 0, 8, 111, 0, 8, 47, 0, 9, 191, 0, 8, 15, 0, 8, 143, 0, 8, 79, 0, 9, 255 ];
+	var fixed_td = [ 80, 5, 1, 87, 5, 257, 83, 5, 17, 91, 5, 4097, 81, 5, 5, 89, 5, 1025, 85, 5, 65, 93, 5, 16385, 80, 5, 3, 88, 5, 513, 84, 5, 33, 92, 5,
+			8193, 82, 5, 9, 90, 5, 2049, 86, 5, 129, 192, 5, 24577, 80, 5, 2, 87, 5, 385, 83, 5, 25, 91, 5, 6145, 81, 5, 7, 89, 5, 1537, 85, 5, 97, 93, 5,
+			24577, 80, 5, 4, 88, 5, 769, 84, 5, 49, 92, 5, 12289, 82, 5, 13, 90, 5, 3073, 86, 5, 193, 192, 5, 24577 ];
+
+	// Tables for deflate from PKZIP's appnote.txt.
+	var cplens = [ // Copy lengths for literal codes 257..285
+	3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258, 0, 0 ];
+
+	// see note #13 above about 258
+	var cplext = [ // Extra bits for literal codes 257..285
+	0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, 112, 112 // 112==invalid
+	];
+
+	var cpdist = [ // Copy offsets for distance codes 0..29
+	1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577 ];
+
+	var cpdext = [ // Extra bits for distance codes
+	0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13 ];
+
+	// If BMAX needs to be larger than 16, then h and x[] should be uLong.
+	var BMAX = 15; // maximum bit length of any code
+
+	function InfTree() {
+		var that = this;
+
+		var hn; // hufts used in space
+		var v; // work area for huft_build
+		var c; // bit length count table
+		var r; // table entry for structure assignment
+		var u; // table stack
+		var x; // bit offsets, then code stack
+
+		function huft_build(b, // code lengths in bits (all assumed <=
+		// BMAX)
+		bindex, n, // number of codes (assumed <= 288)
+		s, // number of simple-valued codes (0..s-1)
+		d, // list of base values for non-simple codes
+		e, // list of extra bits for non-simple codes
+		t, // result: starting table
+		m, // maximum lookup bits, returns actual
+		hp,// space for trees
+		hn,// hufts used in space
+		v // working area: values in order of bit length
+		) {
+			// Given a list of code lengths and a maximum table size, make a set of
+			// tables to decode that set of codes. Return Z_OK on success,
+			// Z_BUF_ERROR
+			// if the given code set is incomplete (the tables are still built in
+			// this
+			// case), Z_DATA_ERROR if the input is invalid (an over-subscribed set
+			// of
+			// lengths), or Z_MEM_ERROR if not enough memory.
+
+			var a; // counter for codes of length k
+			var f; // i repeats in table every f entries
+			var g; // maximum code length
+			var h; // table level
+			var i; // counter, current code
+			var j; // counter
+			var k; // number of bits in current code
+			var l; // bits per table (returned in m)
+			var mask; // (1 << w) - 1, to avoid cc -O bug on HP
+			var p; // pointer into c[], b[], or v[]
+			var q; // points to current table
+			var w; // bits before this table == (l * h)
+			var xp; // pointer into x
+			var y; // number of dummy codes added
+			var z; // number of entries in current table
+
+			// Generate counts for each bit length
+
+			p = 0;
+			i = n;
+			do {
+				c[b[bindex + p]]++;
+				p++;
+				i--; // assume all entries <= BMAX
+			} while (i !== 0);
+
+			if (c[0] == n) { // null input--all zero length codes
+				t[0] = -1;
+				m[0] = 0;
+				return Z_OK;
+			}
+
+			// Find minimum and maximum length, bound *m by those
+			l = m[0];
+			for (j = 1; j <= BMAX; j++)
+				if (c[j] !== 0)
+					break;
+			k = j; // minimum code length
+			if (l < j) {
+				l = j;
+			}
+			for (i = BMAX; i !== 0; i--) {
+				if (c[i] !== 0)
+					break;
+			}
+			g = i; // maximum code length
+			if (l > i) {
+				l = i;
+			}
+			m[0] = l;
+
+			// Adjust last length count to fill out codes, if needed
+			for (y = 1 << j; j < i; j++, y <<= 1) {
+				if ((y -= c[j]) < 0) {
+					return Z_DATA_ERROR;
+				}
+			}
+			if ((y -= c[i]) < 0) {
+				return Z_DATA_ERROR;
+			}
+			c[i] += y;
+
+			// Generate starting offsets into the value table for each length
+			x[1] = j = 0;
+			p = 1;
+			xp = 2;
+			while (--i !== 0) { // note that i == g from above
+				x[xp] = (j += c[p]);
+				xp++;
+				p++;
+			}
+
+			// Make a table of values in order of bit lengths
+			i = 0;
+			p = 0;
+			do {
+				if ((j = b[bindex + p]) !== 0) {
+					v[x[j]++] = i;
+				}
+				p++;
+			} while (++i < n);
+			n = x[g]; // set n to length of v
+
+			// Generate the Huffman codes and for each, make the table entries
+			x[0] = i = 0; // first Huffman code is zero
+			p = 0; // grab values in bit order
+			h = -1; // no tables yet--level -1
+			w = -l; // bits decoded == (l * h)
+			u[0] = 0; // just to keep compilers happy
+			q = 0; // ditto
+			z = 0; // ditto
+
+			// go through the bit lengths (k already is bits in shortest code)
+			for (; k <= g; k++) {
+				a = c[k];
+				while (a-- !== 0) {
+					// here i is the Huffman code of length k bits for value *p
+					// make tables up to required level
+					while (k > w + l) {
+						h++;
+						w += l; // previous table always l bits
+						// compute minimum size table less than or equal to l bits
+						z = g - w;
+						z = (z > l) ? l : z; // table size upper limit
+						if ((f = 1 << (j = k - w)) > a + 1) { // try a k-w bit table
+							// too few codes for
+							// k-w bit table
+							f -= a + 1; // deduct codes from patterns left
+							xp = k;
+							if (j < z) {
+								while (++j < z) { // try smaller tables up to z bits
+									if ((f <<= 1) <= c[++xp])
+										break; // enough codes to use up j bits
+									f -= c[xp]; // else deduct codes from patterns
+								}
+							}
+						}
+						z = 1 << j; // table entries for j-bit table
+
+						// allocate new table
+						if (hn[0] + z > MANY) { // (note: doesn't matter for fixed)
+							return Z_DATA_ERROR; // overflow of MANY
+						}
+						u[h] = q = /* hp+ */hn[0]; // DEBUG
+						hn[0] += z;
+
+						// connect to last table, if there is one
+						if (h !== 0) {
+							x[h] = i; // save pattern for backing up
+							r[0] = /* (byte) */j; // bits in this table
+							r[1] = /* (byte) */l; // bits to dump before this table
+							j = i >>> (w - l);
+							r[2] = /* (int) */(q - u[h - 1] - j); // offset to this table
+							hp.set(r, (u[h - 1] + j) * 3);
+							// to
+							// last
+							// table
+						} else {
+							t[0] = q; // first table is returned result
+						}
+					}
+
+					// set up table entry in r
+					r[1] = /* (byte) */(k - w);
+					if (p >= n) {
+						r[0] = 128 + 64; // out of values--invalid code
+					} else if (v[p] < s) {
+						r[0] = /* (byte) */(v[p] < 256 ? 0 : 32 + 64); // 256 is
+						// end-of-block
+						r[2] = v[p++]; // simple code is just the value
+					} else {
+						r[0] = /* (byte) */(e[v[p] - s] + 16 + 64); // non-simple--look
+						// up in lists
+						r[2] = d[v[p++] - s];
+					}
+
+					// fill code-like entries with r
+					f = 1 << (k - w);
+					for (j = i >>> w; j < z; j += f) {
+						hp.set(r, (q + j) * 3);
+					}
+
+					// backwards increment the k-bit code i
+					for (j = 1 << (k - 1); (i & j) !== 0; j >>>= 1) {
+						i ^= j;
+					}
+					i ^= j;
+
+					// backup over finished tables
+					mask = (1 << w) - 1; // needed on HP, cc -O bug
+					while ((i & mask) != x[h]) {
+						h--; // don't need to update q
+						w -= l;
+						mask = (1 << w) - 1;
+					}
+				}
+			}
+			// Return Z_BUF_ERROR if we were given an incomplete table
+			return y !== 0 && g != 1 ? Z_BUF_ERROR : Z_OK;
+		}
+
+		function initWorkArea(vsize) {
+			var i;
+			if (!hn) {
+				hn = []; // []; //new Array(1);
+				v = []; // new Array(vsize);
+				c = new Int32Array(BMAX + 1); // new Array(BMAX + 1);
+				r = []; // new Array(3);
+				u = new Int32Array(BMAX); // new Array(BMAX);
+				x = new Int32Array(BMAX + 1); // new Array(BMAX + 1);
+			}
+			if (v.length < vsize) {
+				v = []; // new Array(vsize);
+			}
+			for (i = 0; i < vsize; i++) {
+				v[i] = 0;
+			}
+			for (i = 0; i < BMAX + 1; i++) {
+				c[i] = 0;
+			}
+			for (i = 0; i < 3; i++) {
+				r[i] = 0;
+			}
+			// for(int i=0; i<BMAX; i++){u[i]=0;}
+			u.set(c.subarray(0, BMAX), 0);
+			// for(int i=0; i<BMAX+1; i++){x[i]=0;}
+			x.set(c.subarray(0, BMAX + 1), 0);
+		}
+
+		that.inflate_trees_bits = function(c, // 19 code lengths
+		bb, // bits tree desired/actual depth
+		tb, // bits tree result
+		hp, // space for trees
+		z // for messages
+		) {
+			var result;
+			initWorkArea(19);
+			hn[0] = 0;
+			result = huft_build(c, 0, 19, 19, null, null, tb, bb, hp, hn, v);
+
+			if (result == Z_DATA_ERROR) {
+				z.msg = "oversubscribed dynamic bit lengths tree";
+			} else if (result == Z_BUF_ERROR || bb[0] === 0) {
+				z.msg = "incomplete dynamic bit lengths tree";
+				result = Z_DATA_ERROR;
+			}
+			return result;
+		};
+
+		that.inflate_trees_dynamic = function(nl, // number of literal/length codes
+		nd, // number of distance codes
+		c, // that many (total) code lengths
+		bl, // literal desired/actual bit depth
+		bd, // distance desired/actual bit depth
+		tl, // literal/length tree result
+		td, // distance tree result
+		hp, // space for trees
+		z // for messages
+		) {
+			var result;
+
+			// build literal/length tree
+			initWorkArea(288);
+			hn[0] = 0;
+			result = huft_build(c, 0, nl, 257, cplens, cplext, tl, bl, hp, hn, v);
+			if (result != Z_OK || bl[0] === 0) {
+				if (result == Z_DATA_ERROR) {
+					z.msg = "oversubscribed literal/length tree";
+				} else if (result != Z_MEM_ERROR) {
+					z.msg = "incomplete literal/length tree";
+					result = Z_DATA_ERROR;
+				}
+				return result;
+			}
+
+			// build distance tree
+			initWorkArea(288);
+			result = huft_build(c, nl, nd, 0, cpdist, cpdext, td, bd, hp, hn, v);
+
+			if (result != Z_OK || (bd[0] === 0 && nl > 257)) {
+				if (result == Z_DATA_ERROR) {
+					z.msg = "oversubscribed distance tree";
+				} else if (result == Z_BUF_ERROR) {
+					z.msg = "incomplete distance tree";
+					result = Z_DATA_ERROR;
+				} else if (result != Z_MEM_ERROR) {
+					z.msg = "empty distance tree with lengths";
+					result = Z_DATA_ERROR;
+				}
+				return result;
+			}
+
+			return Z_OK;
+		};
+
+	}
+
+	InfTree.inflate_trees_fixed = function(bl, // literal desired/actual bit depth
+	bd, // distance desired/actual bit depth
+	tl,// literal/length tree result
+	td// distance tree result
+	) {
+		bl[0] = fixed_bl;
+		bd[0] = fixed_bd;
+		tl[0] = fixed_tl;
+		td[0] = fixed_td;
+		return Z_OK;
+	};
+
+	// InfCodes
+
+	// waiting for "i:"=input,
+	// "o:"=output,
+	// "x:"=nothing
+	var START = 0; // x: set up for LEN
+	var LEN = 1; // i: get length/literal/eob next
+	var LENEXT = 2; // i: getting length extra (have base)
+	var DIST = 3; // i: get distance next
+	var DISTEXT = 4;// i: getting distance extra
+	var COPY = 5; // o: copying bytes in window, waiting
+	// for space
+	var LIT = 6; // o: got literal, waiting for output
+	// space
+	var WASH = 7; // o: got eob, possibly still output
+	// waiting
+	var END = 8; // x: got eob and all data flushed
+	var BADCODE = 9;// x: got error
+
+	function InfCodes() {
+		var that = this;
+
+		var mode; // current inflate_codes mode
+
+		// mode dependent information
+		var len = 0;
+
+		var tree; // pointer into tree
+		var tree_index = 0;
+		var need = 0; // bits needed
+
+		var lit = 0;
+
+		// if EXT or COPY, where and how much
+		var get = 0; // bits to get for extra
+		var dist = 0; // distance back to copy from
+
+		var lbits = 0; // ltree bits decoded per branch
+		var dbits = 0; // dtree bits decoder per branch
+		var ltree; // literal/length/eob tree
+		var ltree_index = 0; // literal/length/eob tree
+		var dtree; // distance tree
+		var dtree_index = 0; // distance tree
+
+		// Called with number of bytes left to write in window at least 258
+		// (the maximum string length) and number of input bytes available
+		// at least ten. The ten bytes are six bytes for the longest length/
+		// distance pair plus four bytes for overloading the bit buffer.
+
+		function inflate_fast(bl, bd, tl, tl_index, td, td_index, s, z) {
+			var t; // temporary pointer
+			var tp; // temporary pointer
+			var tp_index; // temporary pointer
+			var e; // extra bits or operation
+			var b; // bit buffer
+			var k; // bits in bit buffer
+			var p; // input data pointer
+			var n; // bytes available there
+			var q; // output window write pointer
+			var m; // bytes to end of window or read pointer
+			var ml; // mask for literal/length tree
+			var md; // mask for distance tree
+			var c; // bytes to copy
+			var d; // distance back to copy from
+			var r; // copy source pointer
+
+			var tp_index_t_3; // (tp_index+t)*3
+
+			// load input, output, bit values
+			p = z.next_in_index;
+			n = z.avail_in;
+			b = s.bitb;
+			k = s.bitk;
+			q = s.write;
+			m = q < s.read ? s.read - q - 1 : s.end - q;
+
+			// initialize masks
+			ml = inflate_mask[bl];
+			md = inflate_mask[bd];
+
+			// do until not enough input or output space for fast loop
+			do { // assume called with m >= 258 && n >= 10
+				// get literal/length code
+				while (k < (20)) { // max bits for literal/length code
+					n--;
+					b |= (z.read_byte(p++) & 0xff) << k;
+					k += 8;
+				}
+
+				t = b & ml;
+				tp = tl;
+				tp_index = tl_index;
+				tp_index_t_3 = (tp_index + t) * 3;
+				if ((e = tp[tp_index_t_3]) === 0) {
+					b >>= (tp[tp_index_t_3 + 1]);
+					k -= (tp[tp_index_t_3 + 1]);
+
+					s.window[q++] = /* (byte) */tp[tp_index_t_3 + 2];
+					m--;
+					continue;
+				}
+				do {
+
+					b >>= (tp[tp_index_t_3 + 1]);
+					k -= (tp[tp_index_t_3 + 1]);
+
+					if ((e & 16) !== 0) {
+						e &= 15;
+						c = tp[tp_index_t_3 + 2] + (/* (int) */b & inflate_mask[e]);
+
+						b >>= e;
+						k -= e;
+
+						// decode distance base of block to copy
+						while (k < (15)) { // max bits for distance code
+							n--;
+							b |= (z.read_byte(p++) & 0xff) << k;
+							k += 8;
+						}
+
+						t = b & md;
+						tp = td;
+						tp_index = td_index;
+						tp_index_t_3 = (tp_index + t) * 3;
+						e = tp[tp_index_t_3];
+
+						do {
+
+							b >>= (tp[tp_index_t_3 + 1]);
+							k -= (tp[tp_index_t_3 + 1]);
+
+							if ((e & 16) !== 0) {
+								// get extra bits to add to distance base
+								e &= 15;
+								while (k < (e)) { // get extra bits (up to 13)
+									n--;
+									b |= (z.read_byte(p++) & 0xff) << k;
+									k += 8;
+								}
+
+								d = tp[tp_index_t_3 + 2] + (b & inflate_mask[e]);
+
+								b >>= (e);
+								k -= (e);
+
+								// do the copy
+								m -= c;
+								if (q >= d) { // offset before dest
+									// just copy
+									r = q - d;
+									if (q - r > 0 && 2 > (q - r)) {
+										s.window[q++] = s.window[r++]; // minimum
+										// count is
+										// three,
+										s.window[q++] = s.window[r++]; // so unroll
+										// loop a
+										// little
+										c -= 2;
+									} else {
+										s.window.set(s.window.subarray(r, r + 2), q);
+										q += 2;
+										r += 2;
+										c -= 2;
+									}
+								} else { // else offset after destination
+									r = q - d;
+									do {
+										r += s.end; // force pointer in window
+									} while (r < 0); // covers invalid distances
+									e = s.end - r;
+									if (c > e) { // if source crosses,
+										c -= e; // wrapped copy
+										if (q - r > 0 && e > (q - r)) {
+											do {
+												s.window[q++] = s.window[r++];
+											} while (--e !== 0);
+										} else {
+											s.window.set(s.window.subarray(r, r + e), q);
+											q += e;
+											r += e;
+											e = 0;
+										}
+										r = 0; // copy rest from start of window
+									}
+
+								}
+
+								// copy all or what's left
+								if (q - r > 0 && c > (q - r)) {
+									do {
+										s.window[q++] = s.window[r++];
+									} while (--c !== 0);
+								} else {
+									s.window.set(s.window.subarray(r, r + c), q);
+									q += c;
+									r += c;
+									c = 0;
+								}
+								break;
+							} else if ((e & 64) === 0) {
+								t += tp[tp_index_t_3 + 2];
+								t += (b & inflate_mask[e]);
+								tp_index_t_3 = (tp_index + t) * 3;
+								e = tp[tp_index_t_3];
+							} else {
+								z.msg = "invalid distance code";
+
+								c = z.avail_in - n;
+								c = (k >> 3) < c ? k >> 3 : c;
+								n += c;
+								p -= c;
+								k -= c << 3;
+
+								s.bitb = b;
+								s.bitk = k;
+								z.avail_in = n;
+								z.total_in += p - z.next_in_index;
+								z.next_in_index = p;
+								s.write = q;
+
+								return Z_DATA_ERROR;
+							}
+						} while (true);
+						break;
+					}
+
+					if ((e & 64) === 0) {
+						t += tp[tp_index_t_3 + 2];
+						t += (b & inflate_mask[e]);
+						tp_index_t_3 = (tp_index + t) * 3;
+						if ((e = tp[tp_index_t_3]) === 0) {
+
+							b >>= (tp[tp_index_t_3 + 1]);
+							k -= (tp[tp_index_t_3 + 1]);
+
+							s.window[q++] = /* (byte) */tp[tp_index_t_3 + 2];
+							m--;
+							break;
+						}
+					} else if ((e & 32) !== 0) {
+
+						c = z.avail_in - n;
+						c = (k >> 3) < c ? k >> 3 : c;
+						n += c;
+						p -= c;
+						k -= c << 3;
+
+						s.bitb = b;
+						s.bitk = k;
+						z.avail_in = n;
+						z.total_in += p - z.next_in_index;
+						z.next_in_index = p;
+						s.write = q;
+
+						return Z_STREAM_END;
+					} else {
+						z.msg = "invalid literal/length code";
+
+						c = z.avail_in - n;
+						c = (k >> 3) < c ? k >> 3 : c;
+						n += c;
+						p -= c;
+						k -= c << 3;
+
+						s.bitb = b;
+						s.bitk = k;
+						z.avail_in = n;
+						z.total_in += p - z.next_in_index;
+						z.next_in_index = p;
+						s.write = q;
+
+						return Z_DATA_ERROR;
+					}
+				} while (true);
+			} while (m >= 258 && n >= 10);
+
+			// not enough input or output--restore pointers and return
+			c = z.avail_in - n;
+			c = (k >> 3) < c ? k >> 3 : c;
+			n += c;
+			p -= c;
+			k -= c << 3;
+
+			s.bitb = b;
+			s.bitk = k;
+			z.avail_in = n;
+			z.total_in += p - z.next_in_index;
+			z.next_in_index = p;
+			s.write = q;
+
+			return Z_OK;
+		}
+
+		that.init = function(bl, bd, tl, tl_index, td, td_index) {
+			mode = START;
+			lbits = /* (byte) */bl;
+			dbits = /* (byte) */bd;
+			ltree = tl;
+			ltree_index = tl_index;
+			dtree = td;
+			dtree_index = td_index;
+			tree = null;
+		};
+
+		that.proc = function(s, z, r) {
+			var j; // temporary storage
+			var tindex; // temporary pointer
+			var e; // extra bits or operation
+			var b = 0; // bit buffer
+			var k = 0; // bits in bit buffer
+			var p = 0; // input data pointer
+			var n; // bytes available there
+			var q; // output window write pointer
+			var m; // bytes to end of window or read pointer
+			var f; // pointer to copy strings from
+
+			// copy input/output information to locals (UPDATE macro restores)
+			p = z.next_in_index;
+			n = z.avail_in;
+			b = s.bitb;
+			k = s.bitk;
+			q = s.write;
+			m = q < s.read ? s.read - q - 1 : s.end - q;
+
+			// process input and output based on current state
+			while (true) {
+				switch (mode) {
+				// waiting for "i:"=input, "o:"=output, "x:"=nothing
+				case START: // x: set up for LEN
+					if (m >= 258 && n >= 10) {
+
+						s.bitb = b;
+						s.bitk = k;
+						z.avail_in = n;
+						z.total_in += p - z.next_in_index;
+						z.next_in_index = p;
+						s.write = q;
+						r = inflate_fast(lbits, dbits, ltree, ltree_index, dtree, dtree_index, s, z);
+
+						p = z.next_in_index;
+						n = z.avail_in;
+						b = s.bitb;
+						k = s.bitk;
+						q = s.write;
+						m = q < s.read ? s.read - q - 1 : s.end - q;
+
+						if (r != Z_OK) {
+							mode = r == Z_STREAM_END ? WASH : BADCODE;
+							break;
+						}
+					}
+					need = lbits;
+					tree = ltree;
+					tree_index = ltree_index;
+
+					mode = LEN;
+					/* falls through */
+				case LEN: // i: get length/literal/eob next
+					j = need;
+
+					while (k < (j)) {
+						if (n !== 0)
+							r = Z_OK;
+						else {
+
+							s.bitb = b;
+							s.bitk = k;
+							z.avail_in = n;
+							z.total_in += p - z.next_in_index;
+							z.next_in_index = p;
+							s.write = q;
+							return s.inflate_flush(z, r);
+						}
+						n--;
+						b |= (z.read_byte(p++) & 0xff) << k;
+						k += 8;
+					}
+
+					tindex = (tree_index + (b & inflate_mask[j])) * 3;
+
+					b >>>= (tree[tindex + 1]);
+					k -= (tree[tindex + 1]);
+
+					e = tree[tindex];
+
+					if (e === 0) { // literal
+						lit = tree[tindex + 2];
+						mode = LIT;
+						break;
+					}
+					if ((e & 16) !== 0) { // length
+						get = e & 15;
+						len = tree[tindex + 2];
+						mode = LENEXT;
+						break;
+					}
+					if ((e & 64) === 0) { // next table
+						need = e;
+						tree_index = tindex / 3 + tree[tindex + 2];
+						break;
+					}
+					if ((e & 32) !== 0) { // end of block
+						mode = WASH;
+						break;
+					}
+					mode = BADCODE; // invalid code
+					z.msg = "invalid literal/length code";
+					r = Z_DATA_ERROR;
+
+					s.bitb = b;
+					s.bitk = k;
+					z.avail_in = n;
+					z.total_in += p - z.next_in_index;
+					z.next_in_index = p;
+					s.write = q;
+					return s.inflate_flush(z, r);
+
+				case LENEXT: // i: getting length extra (have base)
+					j = get;
+
+					while (k < (j)) {
+						if (n !== 0)
+							r = Z_OK;
+						else {
+
+							s.bitb = b;
+							s.bitk = k;
+							z.avail_in = n;
+							z.total_in += p - z.next_in_index;
+							z.next_in_index = p;
+							s.write = q;
+							return s.inflate_flush(z, r);
+						}
+						n--;
+						b |= (z.read_byte(p++) & 0xff) << k;
+						k += 8;
+					}
+
+					len += (b & inflate_mask[j]);
+
+					b >>= j;
+					k -= j;
+
+					need = dbits;
+					tree = dtree;
+					tree_index = dtree_index;
+					mode = DIST;
+					/* falls through */
+				case DIST: // i: get distance next
+					j = need;
+
+					while (k < (j)) {
+						if (n !== 0)
+							r = Z_OK;
+						else {
+
+							s.bitb = b;
+							s.bitk = k;
+							z.avail_in = n;
+							z.total_in += p - z.next_in_index;
+							z.next_in_index = p;
+							s.write = q;
+							return s.inflate_flush(z, r);
+						}
+						n--;
+						b |= (z.read_byte(p++) & 0xff) << k;
+						k += 8;
+					}
+
+					tindex = (tree_index + (b & inflate_mask[j])) * 3;
+
+					b >>= tree[tindex + 1];
+					k -= tree[tindex + 1];
+
+					e = (tree[tindex]);
+					if ((e & 16) !== 0) { // distance
+						get = e & 15;
+						dist = tree[tindex + 2];
+						mode = DISTEXT;
+						break;
+					}
+					if ((e & 64) === 0) { // next table
+						need = e;
+						tree_index = tindex / 3 + tree[tindex + 2];
+						break;
+					}
+					mode = BADCODE; // invalid code
+					z.msg = "invalid distance code";
+					r = Z_DATA_ERROR;
+
+					s.bitb = b;
+					s.bitk = k;
+					z.avail_in = n;
+					z.total_in += p - z.next_in_index;
+					z.next_in_index = p;
+					s.write = q;
+					return s.inflate_flush(z, r);
+
+				case DISTEXT: // i: getting distance extra
+					j = get;
+
+					while (k < (j)) {
+						if (n !== 0)
+							r = Z_OK;
+						else {
+
+							s.bitb = b;
+							s.bitk = k;
+							z.avail_in = n;
+							z.total_in += p - z.next_in_index;
+							z.next_in_index = p;
+							s.write = q;
+							return s.inflate_flush(z, r);
+						}
+						n--;
+						b |= (z.read_byte(p++) & 0xff) << k;
+						k += 8;
+					}
+
+					dist += (b & inflate_mask[j]);
+
+					b >>= j;
+					k -= j;
+
+					mode = COPY;
+					/* falls through */
+				case COPY: // o: copying bytes in window, waiting for space
+					f = q - dist;
+					while (f < 0) { // modulo window size-"while" instead
+						f += s.end; // of "if" handles invalid distances
+					}
+					while (len !== 0) {
+
+						if (m === 0) {
+							if (q == s.end && s.read !== 0) {
+								q = 0;
+								m = q < s.read ? s.read - q - 1 : s.end - q;
+							}
+							if (m === 0) {
+								s.write = q;
+								r = s.inflate_flush(z, r);
+								q = s.write;
+								m = q < s.read ? s.read - q - 1 : s.end - q;
+
+								if (q == s.end && s.read !== 0) {
+									q = 0;
+									m = q < s.read ? s.read - q - 1 : s.end - q;
+								}
+
+								if (m === 0) {
+									s.bitb = b;
+									s.bitk = k;
+									z.avail_in = n;
+									z.total_in += p - z.next_in_index;
+									z.next_in_index = p;
+									s.write = q;
+									return s.inflate_flush(z, r);
+								}
+							}
+						}
+
+						s.window[q++] = s.window[f++];
+						m--;
+
+						if (f == s.end)
+							f = 0;
+						len--;
+					}
+					mode = START;
+					break;
+				case LIT: // o: got literal, waiting for output space
+					if (m === 0) {
+						if (q == s.end && s.read !== 0) {
+							q = 0;
+							m = q < s.read ? s.read - q - 1 : s.end - q;
+						}
+						if (m === 0) {
+							s.write = q;
+							r = s.inflate_flush(z, r);
+							q = s.write;
+							m = q < s.read ? s.read - q - 1 : s.end - q;
+
+							if (q == s.end && s.read !== 0) {
+								q = 0;
+								m = q < s.read ? s.read - q - 1 : s.end - q;
+							}
+							if (m === 0) {
+								s.bitb = b;
+								s.bitk = k;
+								z.avail_in = n;
+								z.total_in += p - z.next_in_index;
+								z.next_in_index = p;
+								s.write = q;
+								return s.inflate_flush(z, r);
+							}
+						}
+					}
+					r = Z_OK;
+
+					s.window[q++] = /* (byte) */lit;
+					m--;
+
+					mode = START;
+					break;
+				case WASH: // o: got eob, possibly more output
+					if (k > 7) { // return unused byte, if any
+						k -= 8;
+						n++;
+						p--; // can always return one
+					}
+
+					s.write = q;
+					r = s.inflate_flush(z, r);
+					q = s.write;
+					m = q < s.read ? s.read - q - 1 : s.end - q;
+
+					if (s.read != s.write) {
+						s.bitb = b;
+						s.bitk = k;
+						z.avail_in = n;
+						z.total_in += p - z.next_in_index;
+						z.next_in_index = p;
+						s.write = q;
+						return s.inflate_flush(z, r);
+					}
+					mode = END;
+					/* falls through */
+				case END:
+					r = Z_STREAM_END;
+					s.bitb = b;
+					s.bitk = k;
+					z.avail_in = n;
+					z.total_in += p - z.next_in_index;
+					z.next_in_index = p;
+					s.write = q;
+					return s.inflate_flush(z, r);
+
+				case BADCODE: // x: got error
+
+					r = Z_DATA_ERROR;
+
+					s.bitb = b;
+					s.bitk = k;
+					z.avail_in = n;
+					z.total_in += p - z.next_in_index;
+					z.next_in_index = p;
+					s.write = q;
+					return s.inflate_flush(z, r);
+
+				default:
+					r = Z_STREAM_ERROR;
+
+					s.bitb = b;
+					s.bitk = k;
+					z.avail_in = n;
+					z.total_in += p - z.next_in_index;
+					z.next_in_index = p;
+					s.write = q;
+					return s.inflate_flush(z, r);
+				}
+			}
+		};
+
+		that.free = function() {
+			// ZFREE(z, c);
+		};
+
+	}
+
+	// InfBlocks
+
+	// Table for deflate from PKZIP's appnote.txt.
+	var border = [ // Order of the bit length code lengths
+	16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 ];
+
+	var TYPE = 0; // get type bits (3, including end bit)
+	var LENS = 1; // get lengths for stored
+	var STORED = 2;// processing stored block
+	var TABLE = 3; // get table lengths
+	var BTREE = 4; // get bit lengths tree for a dynamic
+	// block
+	var DTREE = 5; // get length, distance trees for a
+	// dynamic block
+	var CODES = 6; // processing fixed or dynamic block
+	var DRY = 7; // output remaining window bytes
+	var DONELOCKS = 8; // finished last block, done
+	var BADBLOCKS = 9; // ot a data error--stuck here
+
+	function InfBlocks(z, w) {
+		var that = this;
+
+		var mode = TYPE; // current inflate_block mode
+
+		var left = 0; // if STORED, bytes left to copy
+
+		var table = 0; // table lengths (14 bits)
+		var index = 0; // index into blens (or border)
+		var blens; // bit lengths of codes
+		var bb = [ 0 ]; // bit length tree depth
+		var tb = [ 0 ]; // bit length decoding tree
+
+		var codes = new InfCodes(); // if CODES, current state
+
+		var last = 0; // true if this block is the last block
+
+		var hufts = new Int32Array(MANY * 3); // single malloc for tree space
+		var check = 0; // check on output
+		var inftree = new InfTree();
+
+		that.bitk = 0; // bits in bit buffer
+		that.bitb = 0; // bit buffer
+		that.window = new Uint8Array(w); // sliding window
+		that.end = w; // one byte after sliding window
+		that.read = 0; // window read pointer
+		that.write = 0; // window write pointer
+
+		that.reset = function(z, c) {
+			if (c)
+				c[0] = check;
+			// if (mode == BTREE || mode == DTREE) {
+			// }
+			if (mode == CODES) {
+				codes.free(z);
+			}
+			mode = TYPE;
+			that.bitk = 0;
+			that.bitb = 0;
+			that.read = that.write = 0;
+		};
+
+		that.reset(z, null);
+
+		// copy as much as possible from the sliding window to the output area
+		that.inflate_flush = function(z, r) {
+			var n;
+			var p;
+			var q;
+
+			// local copies of source and destination pointers
+			p = z.next_out_index;
+			q = that.read;
+
+			// compute number of bytes to copy as far as end of window
+			n = /* (int) */((q <= that.write ? that.write : that.end) - q);
+			if (n > z.avail_out)
+				n = z.avail_out;
+			if (n !== 0 && r == Z_BUF_ERROR)
+				r = Z_OK;
+
+			// update counters
+			z.avail_out -= n;
+			z.total_out += n;
+
+			// copy as far as end of window
+			z.next_out.set(that.window.subarray(q, q + n), p);
+			p += n;
+			q += n;
+
+			// see if more to copy at beginning of window
+			if (q == that.end) {
+				// wrap pointers
+				q = 0;
+				if (that.write == that.end)
+					that.write = 0;
+
+				// compute bytes to copy
+				n = that.write - q;
+				if (n > z.avail_out)
+					n = z.avail_out;
+				if (n !== 0 && r == Z_BUF_ERROR)
+					r = Z_OK;
+
+				// update counters
+				z.avail_out -= n;
+				z.total_out += n;
+
+				// copy
+				z.next_out.set(that.window.subarray(q, q + n), p);
+				p += n;
+				q += n;
+			}
+
+			// update pointers
+			z.next_out_index = p;
+			that.read = q;
+
+			// done
+			return r;
+		};
+
+		that.proc = function(z, r) {
+			var t; // temporary storage
+			var b; // bit buffer
+			var k; // bits in bit buffer
+			var p; // input data pointer
+			var n; // bytes available there
+			var q; // output window write pointer
+			var m; // bytes to end of window or read pointer
+
+			var i;
+
+			// copy input/output information to locals (UPDATE macro restores)
+			// {
+			p = z.next_in_index;
+			n = z.avail_in;
+			b = that.bitb;
+			k = that.bitk;
+			// }
+			// {
+			q = that.write;
+			m = /* (int) */(q < that.read ? that.read - q - 1 : that.end - q);
+			// }
+
+			// process input based on current state
+			// DEBUG dtree
+			while (true) {
+				switch (mode) {
+				case TYPE:
+
+					while (k < (3)) {
+						if (n !== 0) {
+							r = Z_OK;
+						} else {
+							that.bitb = b;
+							that.bitk = k;
+							z.avail_in = n;
+							z.total_in += p - z.next_in_index;
+							z.next_in_index = p;
+							that.write = q;
+							return that.inflate_flush(z, r);
+						}
+						n--;
+						b |= (z.read_byte(p++) & 0xff) << k;
+						k += 8;
+					}
+					t = /* (int) */(b & 7);
+					last = t & 1;
+
+					switch (t >>> 1) {
+					case 0: // stored
+						// {
+						b >>>= (3);
+						k -= (3);
+						// }
+						t = k & 7; // go to byte boundary
+
+						// {
+						b >>>= (t);
+						k -= (t);
+						// }
+						mode = LENS; // get length of stored block
+						break;
+					case 1: // fixed
+						// {
+						var bl = []; // new Array(1);
+						var bd = []; // new Array(1);
+						var tl = [ [] ]; // new Array(1);
+						var td = [ [] ]; // new Array(1);
+
+						InfTree.inflate_trees_fixed(bl, bd, tl, td);
+						codes.init(bl[0], bd[0], tl[0], 0, td[0], 0);
+						// }
+
+						// {
+						b >>>= (3);
+						k -= (3);
+						// }
+
+						mode = CODES;
+						break;
+					case 2: // dynamic
+
+						// {
+						b >>>= (3);
+						k -= (3);
+						// }
+
+						mode = TABLE;
+						break;
+					case 3: // illegal
+
+						// {
+						b >>>= (3);
+						k -= (3);
+						// }
+						mode = BADBLOCKS;
+						z.msg = "invalid block type";
+						r = Z_DATA_ERROR;
+
+						that.bitb = b;
+						that.bitk = k;
+						z.avail_in = n;
+						z.total_in += p - z.next_in_index;
+						z.next_in_index = p;
+						that.write = q;
+						return that.inflate_flush(z, r);
+					}
+					break;
+				case LENS:
+
+					while (k < (32)) {
+						if (n !== 0) {
+							r = Z_OK;
+						} else {
+							that.bitb = b;
+							that.bitk = k;
+							z.avail_in = n;
+							z.total_in += p - z.next_in_index;
+							z.next_in_index = p;
+							that.write = q;
+							return that.inflate_flush(z, r);
+						}
+						n--;
+						b |= (z.read_byte(p++) & 0xff) << k;
+						k += 8;
+					}
+
+					if ((((~b) >>> 16) & 0xffff) != (b & 0xffff)) {
+						mode = BADBLOCKS;
+						z.msg = "invalid stored block lengths";
+						r = Z_DATA_ERROR;
+
+						that.bitb = b;
+						that.bitk = k;
+						z.avail_in = n;
+						z.total_in += p - z.next_in_index;
+						z.next_in_index = p;
+						that.write = q;
+						return that.inflate_flush(z, r);
+					}
+					left = (b & 0xffff);
+					b = k = 0; // dump bits
+					mode = left !== 0 ? STORED : (last !== 0 ? DRY : TYPE);
+					break;
+				case STORED:
+					if (n === 0) {
+						that.bitb = b;
+						that.bitk = k;
+						z.avail_in = n;
+						z.total_in += p - z.next_in_index;
+						z.next_in_index = p;
+						that.write = q;
+						return that.inflate_flush(z, r);
+					}
+
+					if (m === 0) {
+						if (q == that.end && that.read !== 0) {
+							q = 0;
+							m = /* (int) */(q < that.read ? that.read - q - 1 : that.end - q);
+						}
+						if (m === 0) {
+							that.write = q;
+							r = that.inflate_flush(z, r);
+							q = that.write;
+							m = /* (int) */(q < that.read ? that.read - q - 1 : that.end - q);
+							if (q == that.end && that.read !== 0) {
+								q = 0;
+								m = /* (int) */(q < that.read ? that.read - q - 1 : that.end - q);
+							}
+							if (m === 0) {
+								that.bitb = b;
+								that.bitk = k;
+								z.avail_in = n;
+								z.total_in += p - z.next_in_index;
+								z.next_in_index = p;
+								that.write = q;
+								return that.inflate_flush(z, r);
+							}
+						}
+					}
+					r = Z_OK;
+
+					t = left;
+					if (t > n)
+						t = n;
+					if (t > m)
+						t = m;
+					that.window.set(z.read_buf(p, t), q);
+					p += t;
+					n -= t;
+					q += t;
+					m -= t;
+					if ((left -= t) !== 0)
+						break;
+					mode = last !== 0 ? DRY : TYPE;
+					break;
+				case TABLE:
+
+					while (k < (14)) {
+						if (n !== 0) {
+							r = Z_OK;
+						} else {
+							that.bitb = b;
+							that.bitk = k;
+							z.avail_in = n;
+							z.total_in += p - z.next_in_index;
+							z.next_in_index = p;
+							that.write = q;
+							return that.inflate_flush(z, r);
+						}
+
+						n--;
+						b |= (z.read_byte(p++) & 0xff) << k;
+						k += 8;
+					}
+
+					table = t = (b & 0x3fff);
+					if ((t & 0x1f) > 29 || ((t >> 5) & 0x1f) > 29) {
+						mode = BADBLOCKS;
+						z.msg = "too many length or distance symbols";
+						r = Z_DATA_ERROR;
+
+						that.bitb = b;
+						that.bitk = k;
+						z.avail_in = n;
+						z.total_in += p - z.next_in_index;
+						z.next_in_index = p;
+						that.write = q;
+						return that.inflate_flush(z, r);
+					}
+					t = 258 + (t & 0x1f) + ((t >> 5) & 0x1f);
+					if (!blens || blens.length < t) {
+						blens = []; // new Array(t);
+					} else {
+						for (i = 0; i < t; i++) {
+							blens[i] = 0;
+						}
+					}
+
+					// {
+					b >>>= (14);
+					k -= (14);
+					// }
+
+					index = 0;
+					mode = BTREE;
+					/* falls through */
+				case BTREE:
+					while (index < 4 + (table >>> 10)) {
+						while (k < (3)) {
+							if (n !== 0) {
+								r = Z_OK;
+							} else {
+								that.bitb = b;
+								that.bitk = k;
+								z.avail_in = n;
+								z.total_in += p - z.next_in_index;
+								z.next_in_index = p;
+								that.write = q;
+								return that.inflate_flush(z, r);
+							}
+							n--;
+							b |= (z.read_byte(p++) & 0xff) << k;
+							k += 8;
+						}
+
+						blens[border[index++]] = b & 7;
+
+						// {
+						b >>>= (3);
+						k -= (3);
+						// }
+					}
+
+					while (index < 19) {
+						blens[border[index++]] = 0;
+					}
+
+					bb[0] = 7;
+					t = inftree.inflate_trees_bits(blens, bb, tb, hufts, z);
+					if (t != Z_OK) {
+						r = t;
+						if (r == Z_DATA_ERROR) {
+							blens = null;
+							mode = BADBLOCKS;
+						}
+
+						that.bitb = b;
+						that.bitk = k;
+						z.avail_in = n;
+						z.total_in += p - z.next_in_index;
+						z.next_in_index = p;
+						that.write = q;
+						return that.inflate_flush(z, r);
+					}
+
+					index = 0;
+					mode = DTREE;
+					/* falls through */
+				case DTREE:
+					while (true) {
+						t = table;
+						if (index >= 258 + (t & 0x1f) + ((t >> 5) & 0x1f)) {
+							break;
+						}
+
+						var j, c;
+
+						t = bb[0];
+
+						while (k < (t)) {
+							if (n !== 0) {
+								r = Z_OK;
+							} else {
+								that.bitb = b;
+								that.bitk = k;
+								z.avail_in = n;
+								z.total_in += p - z.next_in_index;
+								z.next_in_index = p;
+								that.write = q;
+								return that.inflate_flush(z, r);
+							}
+							n--;
+							b |= (z.read_byte(p++) & 0xff) << k;
+							k += 8;
+						}
+
+						// if (tb[0] == -1) {
+						// System.err.println("null...");
+						// }
+
+						t = hufts[(tb[0] + (b & inflate_mask[t])) * 3 + 1];
+						c = hufts[(tb[0] + (b & inflate_mask[t])) * 3 + 2];
+
+						if (c < 16) {
+							b >>>= (t);
+							k -= (t);
+							blens[index++] = c;
+						} else { // c == 16..18
+							i = c == 18 ? 7 : c - 14;
+							j = c == 18 ? 11 : 3;
+
+							while (k < (t + i)) {
+								if (n !== 0) {
+									r = Z_OK;
+								} else {
+									that.bitb = b;
+									that.bitk = k;
+									z.avail_in = n;
+									z.total_in += p - z.next_in_index;
+									z.next_in_index = p;
+									that.write = q;
+									return that.inflate_flush(z, r);
+								}
+								n--;
+								b |= (z.read_byte(p++) & 0xff) << k;
+								k += 8;
+							}
+
+							b >>>= (t);
+							k -= (t);
+
+							j += (b & inflate_mask[i]);
+
+							b >>>= (i);
+							k -= (i);
+
+							i = index;
+							t = table;
+							if (i + j > 258 + (t & 0x1f) + ((t >> 5) & 0x1f) || (c == 16 && i < 1)) {
+								blens = null;
+								mode = BADBLOCKS;
+								z.msg = "invalid bit length repeat";
+								r = Z_DATA_ERROR;
+
+								that.bitb = b;
+								that.bitk = k;
+								z.avail_in = n;
+								z.total_in += p - z.next_in_index;
+								z.next_in_index = p;
+								that.write = q;
+								return that.inflate_flush(z, r);
+							}
+
+							c = c == 16 ? blens[i - 1] : 0;
+							do {
+								blens[i++] = c;
+							} while (--j !== 0);
+							index = i;
+						}
+					}
+
+					tb[0] = -1;
+					// {
+					var bl_ = []; // new Array(1);
+					var bd_ = []; // new Array(1);
+					var tl_ = []; // new Array(1);
+					var td_ = []; // new Array(1);
+					bl_[0] = 9; // must be <= 9 for lookahead assumptions
+					bd_[0] = 6; // must be <= 9 for lookahead assumptions
+
+					t = table;
+					t = inftree.inflate_trees_dynamic(257 + (t & 0x1f), 1 + ((t >> 5) & 0x1f), blens, bl_, bd_, tl_, td_, hufts, z);
+
+					if (t != Z_OK) {
+						if (t == Z_DATA_ERROR) {
+							blens = null;
+							mode = BADBLOCKS;
+						}
+						r = t;
+
+						that.bitb = b;
+						that.bitk = k;
+						z.avail_in = n;
+						z.total_in += p - z.next_in_index;
+						z.next_in_index = p;
+						that.write = q;
+						return that.inflate_flush(z, r);
+					}
+					codes.init(bl_[0], bd_[0], hufts, tl_[0], hufts, td_[0]);
+					// }
+					mode = CODES;
+					/* falls through */
+				case CODES:
+					that.bitb = b;
+					that.bitk = k;
+					z.avail_in = n;
+					z.total_in += p - z.next_in_index;
+					z.next_in_index = p;
+					that.write = q;
+
+					if ((r = codes.proc(that, z, r)) != Z_STREAM_END) {
+						return that.inflate_flush(z, r);
+					}
+					r = Z_OK;
+					codes.free(z);
+
+					p = z.next_in_index;
+					n = z.avail_in;
+					b = that.bitb;
+					k = that.bitk;
+					q = that.write;
+					m = /* (int) */(q < that.read ? that.read - q - 1 : that.end - q);
+
+					if (last === 0) {
+						mode = TYPE;
+						break;
+					}
+					mode = DRY;
+					/* falls through */
+				case DRY:
+					that.write = q;
+					r = that.inflate_flush(z, r);
+					q = that.write;
+					m = /* (int) */(q < that.read ? that.read - q - 1 : that.end - q);
+					if (that.read != that.write) {
+						that.bitb = b;
+						that.bitk = k;
+						z.avail_in = n;
+						z.total_in += p - z.next_in_index;
+						z.next_in_index = p;
+						that.write = q;
+						return that.inflate_flush(z, r);
+					}
+					mode = DONELOCKS;
+					/* falls through */
+				case DONELOCKS:
+					r = Z_STREAM_END;
+
+					that.bitb = b;
+					that.bitk = k;
+					z.avail_in = n;
+					z.total_in += p - z.next_in_index;
+					z.next_in_index = p;
+					that.write = q;
+					return that.inflate_flush(z, r);
+				case BADBLOCKS:
+					r = Z_DATA_ERROR;
+
+					that.bitb = b;
+					that.bitk = k;
+					z.avail_in = n;
+					z.total_in += p - z.next_in_index;
+					z.next_in_index = p;
+					that.write = q;
+					return that.inflate_flush(z, r);
+
+				default:
+					r = Z_STREAM_ERROR;
+
+					that.bitb = b;
+					that.bitk = k;
+					z.avail_in = n;
+					z.total_in += p - z.next_in_index;
+					z.next_in_index = p;
+					that.write = q;
+					return that.inflate_flush(z, r);
+				}
+			}
+		};
+
+		that.free = function(z) {
+			that.reset(z, null);
+			that.window = null;
+			hufts = null;
+			// ZFREE(z, s);
+		};
+
+		that.set_dictionary = function(d, start, n) {
+			that.window.set(d.subarray(start, start + n), 0);
+			that.read = that.write = n;
+		};
+
+		// Returns true if inflate is currently at the end of a block generated
+		// by Z_SYNC_FLUSH or Z_FULL_FLUSH.
+		that.sync_point = function() {
+			return mode == LENS ? 1 : 0;
+		};
+
+	}
+
+	// Inflate
+
+	// preset dictionary flag in zlib header
+	var PRESET_DICT = 0x20;
+
+	var Z_DEFLATED = 8;
+
+	var METHOD = 0; // waiting for method byte
+	var FLAG = 1; // waiting for flag byte
+	var DICT4 = 2; // four dictionary check bytes to go
+	var DICT3 = 3; // three dictionary check bytes to go
+	var DICT2 = 4; // two dictionary check bytes to go
+	var DICT1 = 5; // one dictionary check byte to go
+	var DICT0 = 6; // waiting for inflateSetDictionary
+	var BLOCKS = 7; // decompressing blocks
+	var DONE = 12; // finished check, done
+	var BAD = 13; // got an error--stay here
+
+	var mark = [ 0, 0, 0xff, 0xff ];
+
+	function Inflate() {
+		var that = this;
+
+		that.mode = 0; // current inflate mode
+
+		// mode dependent information
+		that.method = 0; // if FLAGS, method byte
+
+		// if CHECK, check values to compare
+		that.was = [ 0 ]; // new Array(1); // computed check value
+		that.need = 0; // stream check value
+
+		// if BAD, inflateSync's marker bytes count
+		that.marker = 0;
+
+		// mode independent information
+		that.wbits = 0; // log2(window size) (8..15, defaults to 15)
+
+		// this.blocks; // current inflate_blocks state
+
+		function inflateReset(z) {
+			if (!z || !z.istate)
+				return Z_STREAM_ERROR;
+
+			z.total_in = z.total_out = 0;
+			z.msg = null;
+			z.istate.mode = BLOCKS;
+			z.istate.blocks.reset(z, null);
+			return Z_OK;
+		}
+
+		that.inflateEnd = function(z) {
+			if (that.blocks)
+				that.blocks.free(z);
+			that.blocks = null;
+			// ZFREE(z, z->state);
+			return Z_OK;
+		};
+
+		that.inflateInit = function(z, w) {
+			z.msg = null;
+			that.blocks = null;
+
+			// set window size
+			if (w < 8 || w > 15) {
+				that.inflateEnd(z);
+				return Z_STREAM_ERROR;
+			}
+			that.wbits = w;
+
+			z.istate.blocks = new InfBlocks(z, 1 << w);
+
+			// reset state
+			inflateReset(z);
+			return Z_OK;
+		};
+
+		that.inflate = function(z, f) {
+			var r;
+			var b;
+
+			if (!z || !z.istate || !z.next_in)
+				return Z_STREAM_ERROR;
+			f = f == Z_FINISH ? Z_BUF_ERROR : Z_OK;
+			r = Z_BUF_ERROR;
+			while (true) {
+				// System.out.println("mode: "+z.istate.mode);
+				switch (z.istate.mode) {
+				case METHOD:
+
+					if (z.avail_in === 0)
+						return r;
+					r = f;
+
+					z.avail_in--;
+					z.total_in++;
+					if (((z.istate.method = z.read_byte(z.next_in_index++)) & 0xf) != Z_DEFLATED) {
+						z.istate.mode = BAD;
+						z.msg = "unknown compression method";
+						z.istate.marker = 5; // can't try inflateSync
+						break;
+					}
+					if ((z.istate.method >> 4) + 8 > z.istate.wbits) {
+						z.istate.mode = BAD;
+						z.msg = "invalid window size";
+						z.istate.marker = 5; // can't try inflateSync
+						break;
+					}
+					z.istate.mode = FLAG;
+					/* falls through */
+				case FLAG:
+
+					if (z.avail_in === 0)
+						return r;
+					r = f;
+
+					z.avail_in--;
+					z.total_in++;
+					b = (z.read_byte(z.next_in_index++)) & 0xff;
+
+					if ((((z.istate.method << 8) + b) % 31) !== 0) {
+						z.istate.mode = BAD;
+						z.msg = "incorrect header check";
+						z.istate.marker = 5; // can't try inflateSync
+						break;
+					}
+
+					if ((b & PRESET_DICT) === 0) {
+						z.istate.mode = BLOCKS;
+						break;
+					}
+					z.istate.mode = DICT4;
+					/* falls through */
+				case DICT4:
+
+					if (z.avail_in === 0)
+						return r;
+					r = f;
+
+					z.avail_in--;
+					z.total_in++;
+					z.istate.need = ((z.read_byte(z.next_in_index++) & 0xff) << 24) & 0xff000000;
+					z.istate.mode = DICT3;
+					/* falls through */
+				case DICT3:
+
+					if (z.avail_in === 0)
+						return r;
+					r = f;
+
+					z.avail_in--;
+					z.total_in++;
+					z.istate.need += ((z.read_byte(z.next_in_index++) & 0xff) << 16) & 0xff0000;
+					z.istate.mode = DICT2;
+					/* falls through */
+				case DICT2:
+
+					if (z.avail_in === 0)
+						return r;
+					r = f;
+
+					z.avail_in--;
+					z.total_in++;
+					z.istate.need += ((z.read_byte(z.next_in_index++) & 0xff) << 8) & 0xff00;
+					z.istate.mode = DICT1;
+					/* falls through */
+				case DICT1:
+
+					if (z.avail_in === 0)
+						return r;
+					r = f;
+
+					z.avail_in--;
+					z.total_in++;
+					z.istate.need += (z.read_byte(z.next_in_index++) & 0xff);
+					z.istate.mode = DICT0;
+					return Z_NEED_DICT;
+				case DICT0:
+					z.istate.mode = BAD;
+					z.msg = "need dictionary";
+					z.istate.marker = 0; // can try inflateSync
+					return Z_STREAM_ERROR;
+				case BLOCKS:
+
+					r = z.istate.blocks.proc(z, r);
+					if (r == Z_DATA_ERROR) {
+						z.istate.mode = BAD;
+						z.istate.marker = 0; // can try inflateSync
+						break;
+					}
+					if (r == Z_OK) {
+						r = f;
+					}
+					if (r != Z_STREAM_END) {
+						return r;
+					}
+					r = f;
+					z.istate.blocks.reset(z, z.istate.was);
+					z.istate.mode = DONE;
+					/* falls through */
+				case DONE:
+					return Z_STREAM_END;
+				case BAD:
+					return Z_DATA_ERROR;
+				default:
+					return Z_STREAM_ERROR;
+				}
+			}
+		};
+
+		that.inflateSetDictionary = function(z, dictionary, dictLength) {
+			var index = 0;
+			var length = dictLength;
+			if (!z || !z.istate || z.istate.mode != DICT0)
+				return Z_STREAM_ERROR;
+
+			if (length >= (1 << z.istate.wbits)) {
+				length = (1 << z.istate.wbits) - 1;
+				index = dictLength - length;
+			}
+			z.istate.blocks.set_dictionary(dictionary, index, length);
+			z.istate.mode = BLOCKS;
+			return Z_OK;
+		};
+
+		that.inflateSync = function(z) {
+			var n; // number of bytes to look at
+			var p; // pointer to bytes
+			var m; // number of marker bytes found in a row
+			var r, w; // temporaries to save total_in and total_out
+
+			// set up
+			if (!z || !z.istate)
+				return Z_STREAM_ERROR;
+			if (z.istate.mode != BAD) {
+				z.istate.mode = BAD;
+				z.istate.marker = 0;
+			}
+			if ((n = z.avail_in) === 0)
+				return Z_BUF_ERROR;
+			p = z.next_in_index;
+			m = z.istate.marker;
+
+			// search
+			while (n !== 0 && m < 4) {
+				if (z.read_byte(p) == mark[m]) {
+					m++;
+				} else if (z.read_byte(p) !== 0) {
+					m = 0;
+				} else {
+					m = 4 - m;
+				}
+				p++;
+				n--;
+			}
+
+			// restore
+			z.total_in += p - z.next_in_index;
+			z.next_in_index = p;
+			z.avail_in = n;
+			z.istate.marker = m;
+
+			// return no joy or set up to restart on a new block
+			if (m != 4) {
+				return Z_DATA_ERROR;
+			}
+			r = z.total_in;
+			w = z.total_out;
+			inflateReset(z);
+			z.total_in = r;
+			z.total_out = w;
+			z.istate.mode = BLOCKS;
+			return Z_OK;
+		};
+
+		// Returns true if inflate is currently at the end of a block generated
+		// by Z_SYNC_FLUSH or Z_FULL_FLUSH. This function is used by one PPP
+		// implementation to provide an additional safety check. PPP uses
+		// Z_SYNC_FLUSH
+		// but removes the length bytes of the resulting empty stored block. When
+		// decompressing, PPP checks that at the end of input packet, inflate is
+		// waiting for these length bytes.
+		that.inflateSyncPoint = function(z) {
+			if (!z || !z.istate || !z.istate.blocks)
+				return Z_STREAM_ERROR;
+			return z.istate.blocks.sync_point();
+		};
+	}
+
+	// ZStream
+
+	function ZStream() {
+	}
+
+	ZStream.prototype = {
+		inflateInit : function(bits) {
+			var that = this;
+			that.istate = new Inflate();
+			if (!bits)
+				bits = MAX_BITS;
+			return that.istate.inflateInit(that, bits);
+		},
+
+		inflate : function(f) {
+			var that = this;
+			if (!that.istate)
+				return Z_STREAM_ERROR;
+			return that.istate.inflate(that, f);
+		},
+
+		inflateEnd : function() {
+			var that = this;
+			if (!that.istate)
+				return Z_STREAM_ERROR;
+			var ret = that.istate.inflateEnd(that);
+			that.istate = null;
+			return ret;
+		},
+
+		inflateSync : function() {
+			var that = this;
+			if (!that.istate)
+				return Z_STREAM_ERROR;
+			return that.istate.inflateSync(that);
+		},
+		inflateSetDictionary : function(dictionary, dictLength) {
+			var that = this;
+			if (!that.istate)
+				return Z_STREAM_ERROR;
+			return that.istate.inflateSetDictionary(that, dictionary, dictLength);
+		},
+		read_byte : function(start) {
+			var that = this;
+			return that.next_in.subarray(start, start + 1)[0];
+		},
+		read_buf : function(start, size) {
+			var that = this;
+			return that.next_in.subarray(start, start + size);
+		}
+	};
+
+	// Inflater
+
+	function Inflater() {
+		var that = this;
+		var z = new ZStream();
+		var bufsize = 512;
+		var flush = Z_NO_FLUSH;
+		var buf = new Uint8Array(bufsize);
+		var nomoreinput = false;
+
+		z.inflateInit();
+		z.next_out = buf;
+
+		that.append = function(data, onprogress) {
+			var err, buffers = [], lastIndex = 0, bufferIndex = 0, bufferSize = 0, array;
+			if (data.length === 0)
+				return;
+			z.next_in_index = 0;
+			z.next_in = data;
+			z.avail_in = data.length;
+			do {
+				z.next_out_index = 0;
+				z.avail_out = bufsize;
+				if ((z.avail_in === 0) && (!nomoreinput)) { // if buffer is empty and more input is available, refill it
+					z.next_in_index = 0;
+					nomoreinput = true;
+				}
+				err = z.inflate(flush);
+				if (nomoreinput && (err === Z_BUF_ERROR)) {
+					if (z.avail_in !== 0)
+						throw new Error("inflating: bad input");
+				} else if (err !== Z_OK && err !== Z_STREAM_END)
+					throw new Error("inflating: " + z.msg);
+				if ((nomoreinput || err === Z_STREAM_END) && (z.avail_in === data.length))
+					throw new Error("inflating: bad input");
+				if (z.next_out_index)
+					if (z.next_out_index === bufsize)
+						buffers.push(new Uint8Array(buf));
+					else
+						buffers.push(new Uint8Array(buf.subarray(0, z.next_out_index)));
+				bufferSize += z.next_out_index;
+				if (onprogress && z.next_in_index > 0 && z.next_in_index != lastIndex) {
+					onprogress(z.next_in_index);
+					lastIndex = z.next_in_index;
+				}
+			} while (z.avail_in > 0 || z.avail_out === 0);
+			array = new Uint8Array(bufferSize);
+			buffers.forEach(function(chunk) {
+				array.set(chunk, bufferIndex);
+				bufferIndex += chunk.length;
+			});
+			return array;
+		};
+		that.flush = function() {
+			z.inflateEnd();
+		};
+	}
+
+	// 'zip' may not be defined in z-worker and some tests
+	var env = global.zip || global;
+	env.Inflater = env._jzlib_Inflater = Inflater;
+})(this);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mda_heatmap_viz/static/javascript/lib/jspdf.debug.js	Thu Apr 07 14:43:37 2016 -0400
@@ -0,0 +1,9359 @@
+/** @preserve
+ * jsPDF - PDF Document creation from JavaScript
+ * Version 1.0.272-git Built on 2014-09-29T15:09
+ *                           CommitID d4770725ca
+ *
+ * Copyright (c) 2010-2014 James Hall, https://github.com/MrRio/jsPDF
+ *               2010 Aaron Spike, https://github.com/acspike
+ *               2012 Willow Systems Corporation, willow-systems.com
+ *               2012 Pablo Hess, https://github.com/pablohess
+ *               2012 Florian Jenett, https://github.com/fjenett
+ *               2013 Warren Weckesser, https://github.com/warrenweckesser
+ *               2013 Youssef Beddad, https://github.com/lifof
+ *               2013 Lee Driscoll, https://github.com/lsdriscoll
+ *               2013 Stefan Slonevskiy, https://github.com/stefslon
+ *               2013 Jeremy Morel, https://github.com/jmorel
+ *               2013 Christoph Hartmann, https://github.com/chris-rock
+ *               2014 Juan Pablo Gaviria, https://github.com/juanpgaviria
+ *               2014 James Makes, https://github.com/dollaruw
+ *               2014 Diego Casorran, https://github.com/diegocr
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * Contributor(s):
+ *    siefkenj, ahwolf, rickygu, Midnith, saintclair, eaparango,
+ *    kim3er, mfo, alnorth,
+ */
+
+/**
+ * Creates new jsPDF document object instance.
+ *
+ * @class
+ * @param orientation One of "portrait" or "landscape" (or shortcuts "p" (Default), "l")
+ * @param unit        Measurement unit to be used when coordinates are specified.
+ *                    One of "pt" (points), "mm" (Default), "cm", "in"
+ * @param format      One of 'pageFormats' as shown below, default: a4
+ * @returns {jsPDF}
+ * @name jsPDF
+ */
+var jsPDF = (function(global) {
+	'use strict';
+	var pdfVersion = '1.3',
+		pageFormats = { // Size in pt of various paper formats
+			'a0'  : [2383.94, 3370.39], 'a1'  : [1683.78, 2383.94],
+			'a2'  : [1190.55, 1683.78], 'a3'  : [ 841.89, 1190.55],
+			'a4'  : [ 595.28,  841.89], 'a5'  : [ 419.53,  595.28],
+			'a6'  : [ 297.64,  419.53], 'a7'  : [ 209.76,  297.64],
+			'a8'  : [ 147.40,  209.76], 'a9'  : [ 104.88,  147.40],
+			'a10' : [  73.70,  104.88], 'b0'  : [2834.65, 4008.19],
+			'b1'  : [2004.09, 2834.65], 'b2'  : [1417.32, 2004.09],
+			'b3'  : [1000.63, 1417.32], 'b4'  : [ 708.66, 1000.63],
+			'b5'  : [ 498.90,  708.66], 'b6'  : [ 354.33,  498.90],
+			'b7'  : [ 249.45,  354.33], 'b8'  : [ 175.75,  249.45],
+			'b9'  : [ 124.72,  175.75], 'b10' : [  87.87,  124.72],
+			'c0'  : [2599.37, 3676.54], 'c1'  : [1836.85, 2599.37],
+			'c2'  : [1298.27, 1836.85], 'c3'  : [ 918.43, 1298.27],
+			'c4'  : [ 649.13,  918.43], 'c5'  : [ 459.21,  649.13],
+			'c6'  : [ 323.15,  459.21], 'c7'  : [ 229.61,  323.15],
+			'c8'  : [ 161.57,  229.61], 'c9'  : [ 113.39,  161.57],
+			'c10' : [  79.37,  113.39], 'dl'  : [ 311.81,  623.62],
+			'letter'            : [612,   792],
+			'government-letter' : [576,   756],
+			'legal'             : [612,  1008],
+			'junior-legal'      : [576,   360],
+			'ledger'            : [1224,  792],
+			'tabloid'           : [792,  1224],
+			'credit-card'       : [153,   243]
+		};
+
+	/**
+	 * jsPDF's Internal PubSub Implementation.
+	 * See mrrio.github.io/jsPDF/doc/symbols/PubSub.html
+	 * Backward compatible rewritten on 2014 by
+	 * Diego Casorran, https://github.com/diegocr
+	 *
+	 * @class
+	 * @name PubSub
+	 */
+	function PubSub(context) {
+		var topics = {};
+
+		this.subscribe = function(topic, callback, once) {
+			if(typeof callback !== 'function') {
+				return false;
+			}
+
+			if(!topics.hasOwnProperty(topic)) {
+				topics[topic] = {};
+			}
+
+			var id = Math.random().toString(35);
+			topics[topic][id] = [callback,!!once];
+
+			return id;
+		};
+
+		this.unsubscribe = function(token) {
+			for(var topic in topics) {
+				if(topics[topic][token]) {
+					delete topics[topic][token];
+					return true;
+				}
+			}
+			return false;
+		};
+
+		this.publish = function(topic) {
+			if(topics.hasOwnProperty(topic)) {
+				var args = Array.prototype.slice.call(arguments, 1), idr = [];
+
+				for(var id in topics[topic]) {
+					var sub = topics[topic][id];
+					try {
+						sub[0].apply(context, args);
+					} catch(ex) {
+						if(global.console) {
+							console.error('jsPDF PubSub Error', ex.message, ex);
+						}
+					}
+					if(sub[1]) idr.push(id);
+				}
+				if(idr.length) idr.forEach(this.unsubscribe);
+			}
+		};
+	}
+
+	/**
+	 * @constructor
+	 * @private
+	 */
+	function jsPDF(orientation, unit, format, compressPdf) {
+		var options = {};
+
+		if (typeof orientation === 'object') {
+			options = orientation;
+
+			orientation = options.orientation;
+			unit = options.unit || unit;
+			format = options.format || format;
+			compressPdf = options.compress || options.compressPdf || compressPdf;
+		}
+
+		// Default options
+		unit        = unit || 'mm';
+		format      = format || 'a4';
+		orientation = ('' + (orientation || 'P')).toLowerCase();
+
+		var format_as_string = ('' + format).toLowerCase(),
+			compress = !!compressPdf && typeof Uint8Array === 'function',
+			textColor            = options.textColor  || '0 g',
+			drawColor            = options.drawColor  || '0 G',
+			activeFontSize       = options.fontSize   || 16,
+			lineHeightProportion = options.lineHeight || 1.15,
+			lineWidth            = options.lineWidth  || 0.200025, // 2mm
+			objectNumber =  2,  // 'n' Current object number
+			outToPages   = !1,  // switches where out() prints. outToPages true = push to pages obj. outToPages false = doc builder content
+			offsets      = [],  // List of offsets. Activated and reset by buildDocument(). Pupulated by various calls buildDocument makes.
+			fonts        = {},  // collection of font objects, where key is fontKey - a dynamically created label for a given font.
+			fontmap      = {},  // mapping structure fontName > fontStyle > font key - performance layer. See addFont()
+			activeFontKey,      // will be string representing the KEY of the font as combination of fontName + fontStyle
+			k,                  // Scale factor
+			tmp,
+			page = 0,
+			currentPage,
+			pages = [],
+			pagedim = {},
+			content = [],
+			lineCapID = 0,
+			lineJoinID = 0,
+			content_length = 0,
+			pageWidth,
+			pageHeight,
+			pageMode,
+			zoomMode,
+			layoutMode,
+			documentProperties = {
+				'title'    : '',
+				'subject'  : '',
+				'author'   : '',
+				'keywords' : '',
+				'creator'  : ''
+			},
+			API = {},
+			events = new PubSub(API),
+
+		/////////////////////
+		// Private functions
+		/////////////////////
+		f2 = function(number) {
+			return number.toFixed(2); // Ie, %.2f
+		},
+		f3 = function(number) {
+			return number.toFixed(3); // Ie, %.3f
+		},
+		padd2 = function(number) {
+			return ('0' + parseInt(number)).slice(-2);
+		},
+		out = function(string) {
+			if (outToPages) {
+				/* set by beginPage */
+				pages[currentPage].push(string);
+			} else {
+				// +1 for '\n' that will be used to join 'content'
+				content_length += string.length + 1;
+				content.push(string);
+			}
+		},
+		newObject = function() {
+			// Begin a new object
+			objectNumber++;
+			offsets[objectNumber] = content_length;
+			out(objectNumber + ' 0 obj');
+			return objectNumber;
+		},
+		putStream = function(str) {
+			out('stream');
+			out(str);
+			out('endstream');
+		},
+		putPages = function() {
+			var n,p,arr,i,deflater,adler32,adler32cs,wPt,hPt;
+
+			adler32cs = global.adler32cs || jsPDF.adler32cs;
+			if (compress && typeof adler32cs === 'undefined') {
+				compress = false;
+			}
+
+			// outToPages = false as set in endDocument(). out() writes to content.
+
+			for (n = 1; n <= page; n++) {
+				newObject();
+				wPt = (pageWidth = pagedim[n].width) * k;
+				hPt = (pageHeight = pagedim[n].height) * k;
+				out('<</Type /Page');
+				out('/Parent 1 0 R');
+				out('/Resources 2 0 R');
+				out('/MediaBox [0 0 ' + f2(wPt) + ' ' + f2(hPt) + ']');
+				out('/Contents ' + (objectNumber + 1) + ' 0 R>>');
+				out('endobj');
+
+				// Page content
+				p = pages[n].join('\n');
+				newObject();
+				if (compress) {
+					arr = [];
+					i = p.length;
+					while(i--) {
+						arr[i] = p.charCodeAt(i);
+					}
+					adler32 = adler32cs.from(p);
+					deflater = new Deflater(6);
+					deflater.append(new Uint8Array(arr));
+					p = deflater.flush();
+					arr = new Uint8Array(p.length + 6);
+					arr.set(new Uint8Array([120, 156])),
+					arr.set(p, 2);
+					arr.set(new Uint8Array([adler32 & 0xFF, (adler32 >> 8) & 0xFF, (adler32 >> 16) & 0xFF, (adler32 >> 24) & 0xFF]), p.length+2);
+					p = String.fromCharCode.apply(null, arr);
+					out('<</Length ' + p.length + ' /Filter [/FlateDecode]>>');
+				} else {
+					out('<</Length ' + p.length + '>>');
+				}
+				putStream(p);
+				out('endobj');
+			}
+			offsets[1] = content_length;
+			out('1 0 obj');
+			out('<</Type /Pages');
+			var kids = '/Kids [';
+			for (i = 0; i < page; i++) {
+				kids += (3 + 2 * i) + ' 0 R ';
+			}
+			out(kids + ']');
+			out('/Count ' + page);
+			out('>>');
+			out('endobj');
+		},
+		putFont = function(font) {
+			font.objectNumber = newObject();
+			out('<</BaseFont/' + font.PostScriptName + '/Type/Font');
+			if (typeof font.encoding === 'string') {
+				out('/Encoding/' + font.encoding);
+			}
+			out('/Subtype/Type1>>');
+			out('endobj');
+		},
+		putFonts = function() {
+			for (var fontKey in fonts) {
+				if (fonts.hasOwnProperty(fontKey)) {
+					putFont(fonts[fontKey]);
+				}
+			}
+		},
+		putXobjectDict = function() {
+			// Loop through images, or other data objects
+			events.publish('putXobjectDict');
+		},
+		putResourceDictionary = function() {
+			out('/ProcSet [/PDF /Text /ImageB /ImageC /ImageI]');
+			out('/Font <<');
+
+			// Do this for each font, the '1' bit is the index of the font
+			for (var fontKey in fonts) {
+				if (fonts.hasOwnProperty(fontKey)) {
+					out('/' + fontKey + ' ' + fonts[fontKey].objectNumber + ' 0 R');
+				}
+			}
+			out('>>');
+			out('/XObject <<');
+			putXobjectDict();
+			out('>>');
+		},
+		putResources = function() {
+			putFonts();
+			events.publish('putResources');
+			// Resource dictionary
+			offsets[2] = content_length;
+			out('2 0 obj');
+			out('<<');
+			putResourceDictionary();
+			out('>>');
+			out('endobj');
+			events.publish('postPutResources');
+		},
+		addToFontDictionary = function(fontKey, fontName, fontStyle) {
+			// this is mapping structure for quick font key lookup.
+			// returns the KEY of the font (ex: "F1") for a given
+			// pair of font name and type (ex: "Arial". "Italic")
+			if (!fontmap.hasOwnProperty(fontName)) {
+				fontmap[fontName] = {};
+			}
+			fontmap[fontName][fontStyle] = fontKey;
+		},
+		/**
+		 * FontObject describes a particular font as member of an instnace of jsPDF
+		 *
+		 * It's a collection of properties like 'id' (to be used in PDF stream),
+		 * 'fontName' (font's family name), 'fontStyle' (font's style variant label)
+		 *
+		 * @class
+		 * @public
+		 * @property id {String} PDF-document-instance-specific label assinged to the font.
+		 * @property PostScriptName {String} PDF specification full name for the font
+		 * @property encoding {Object} Encoding_name-to-Font_metrics_object mapping.
+		 * @name FontObject
+		 */
+		addFont = function(PostScriptName, fontName, fontStyle, encoding) {
+			var fontKey = 'F' + (Object.keys(fonts).length + 1).toString(10),
+			// This is FontObject
+			font = fonts[fontKey] = {
+				'id'             : fontKey,
+				'PostScriptName' : PostScriptName,
+				'fontName'       : fontName,
+				'fontStyle'      : fontStyle,
+				'encoding'       : encoding,
+				'metadata'       : {}
+			};
+			addToFontDictionary(fontKey, fontName, fontStyle);
+			events.publish('addFont', font);
+
+			return fontKey;
+		},
+		addFonts = function() {
+
+			var HELVETICA     = "helvetica",
+				TIMES         = "times",
+				COURIER       = "courier",
+				NORMAL        = "normal",
+				BOLD          = "bold",
+				ITALIC        = "italic",
+				BOLD_ITALIC   = "bolditalic",
+				encoding      = 'StandardEncoding',
+				standardFonts = [
+					['Helvetica', HELVETICA, NORMAL],
+					['Helvetica-Bold', HELVETICA, BOLD],
+					['Helvetica-Oblique', HELVETICA, ITALIC],
+					['Helvetica-BoldOblique', HELVETICA, BOLD_ITALIC],
+					['Courier', COURIER, NORMAL],
+					['Courier-Bold', COURIER, BOLD],
+					['Courier-Oblique', COURIER, ITALIC],
+					['Courier-BoldOblique', COURIER, BOLD_ITALIC],
+					['Times-Roman', TIMES, NORMAL],
+					['Times-Bold', TIMES, BOLD],
+					['Times-Italic', TIMES, ITALIC],
+					['Times-BoldItalic', TIMES, BOLD_ITALIC]
+				];
+
+			for (var i = 0, l = standardFonts.length; i < l; i++) {
+				var fontKey = addFont(
+						standardFonts[i][0],
+						standardFonts[i][1],
+						standardFonts[i][2],
+						encoding);
+
+				// adding aliases for standard fonts, this time matching the capitalization
+				var parts = standardFonts[i][0].split('-');
+				addToFontDictionary(fontKey, parts[0], parts[1] || '');
+			}
+			events.publish('addFonts', { fonts : fonts, dictionary : fontmap });
+		},
+		SAFE = function __safeCall(fn) {
+			fn.foo = function __safeCallWrapper() {
+				try {
+					return fn.apply(this, arguments);
+				} catch (e) {
+					var stack = e.stack || '';
+					if(~stack.indexOf(' at ')) stack = stack.split(" at ")[1];
+					var m = "Error in function " + stack.split("\n")[0].split('<')[0] + ": " + e.message;
+					if(global.console) {
+						global.console.error(m, e);
+						if(global.alert) alert(m);
+					} else {
+						throw new Error(m);
+					}
+				}
+			};
+			fn.foo.bar = fn;
+			return fn.foo;
+		},
+		to8bitStream = function(text, flags) {
+		/**
+		 * PDF 1.3 spec:
+		 * "For text strings encoded in Unicode, the first two bytes must be 254 followed by
+		 * 255, representing the Unicode byte order marker, U+FEFF. (This sequence conflicts
+		 * with the PDFDocEncoding character sequence thorn ydieresis, which is unlikely
+		 * to be a meaningful beginning of a word or phrase.) The remainder of the
+		 * string consists of Unicode character codes, according to the UTF-16 encoding
+		 * specified in the Unicode standard, version 2.0. Commonly used Unicode values
+		 * are represented as 2 bytes per character, with the high-order byte appearing first
+		 * in the string."
+		 *
+		 * In other words, if there are chars in a string with char code above 255, we
+		 * recode the string to UCS2 BE - string doubles in length and BOM is prepended.
+		 *
+		 * HOWEVER!
+		 * Actual *content* (body) text (as opposed to strings used in document properties etc)
+		 * does NOT expect BOM. There, it is treated as a literal GID (Glyph ID)
+		 *
+		 * Because of Adobe's focus on "you subset your fonts!" you are not supposed to have
+		 * a font that maps directly Unicode (UCS2 / UTF16BE) code to font GID, but you could
+		 * fudge it with "Identity-H" encoding and custom CIDtoGID map that mimics Unicode
+		 * code page. There, however, all characters in the stream are treated as GIDs,
+		 * including BOM, which is the reason we need to skip BOM in content text (i.e. that
+		 * that is tied to a font).
+		 *
+		 * To signal this "special" PDFEscape / to8bitStream handling mode,
+		 * API.text() function sets (unless you overwrite it with manual values
+		 * given to API.text(.., flags) )
+		 * flags.autoencode = true
+		 * flags.noBOM = true
+		 *
+		 * ===================================================================================
+		 * `flags` properties relied upon:
+		 *   .sourceEncoding = string with encoding label.
+		 *                     "Unicode" by default. = encoding of the incoming text.
+		 *                     pass some non-existing encoding name
+		 *                     (ex: 'Do not touch my strings! I know what I am doing.')
+		 *                     to make encoding code skip the encoding step.
+		 *   .outputEncoding = Either valid PDF encoding name
+		 *                     (must be supported by jsPDF font metrics, otherwise no encoding)
+		 *                     or a JS object, where key = sourceCharCode, value = outputCharCode
+		 *                     missing keys will be treated as: sourceCharCode === outputCharCode
+		 *   .noBOM
+		 *       See comment higher above for explanation for why this is important
+		 *   .autoencode
+		 *       See comment higher above for explanation for why this is important
+		 */
+
+			var i,l,sourceEncoding,encodingBlock,outputEncoding,newtext,isUnicode,ch,bch;
+
+			flags = flags || {};
+			sourceEncoding = flags.sourceEncoding || 'Unicode';
+			outputEncoding = flags.outputEncoding;
+
+			// This 'encoding' section relies on font metrics format
+			// attached to font objects by, among others,
+			// "Willow Systems' standard_font_metrics plugin"
+			// see jspdf.plugin.standard_font_metrics.js for format
+			// of the font.metadata.encoding Object.
+			// It should be something like
+			//   .encoding = {'codePages':['WinANSI....'], 'WinANSI...':{code:code, ...}}
+			//   .widths = {0:width, code:width, ..., 'fof':divisor}
+			//   .kerning = {code:{previous_char_code:shift, ..., 'fof':-divisor},...}
+			if ((flags.autoencode || outputEncoding) &&
+				fonts[activeFontKey].metadata &&
+				fonts[activeFontKey].metadata[sourceEncoding] &&
+				fonts[activeFontKey].metadata[sourceEncoding].encoding) {
+				encodingBlock = fonts[activeFontKey].metadata[sourceEncoding].encoding;
+
+				// each font has default encoding. Some have it clearly defined.
+				if (!outputEncoding && fonts[activeFontKey].encoding) {
+					outputEncoding = fonts[activeFontKey].encoding;
+				}
+
+				// Hmmm, the above did not work? Let's try again, in different place.
+				if (!outputEncoding && encodingBlock.codePages) {
+					outputEncoding = encodingBlock.codePages[0]; // let's say, first one is the default
+				}
+
+				if (typeof outputEncoding === 'string') {
+					outputEncoding = encodingBlock[outputEncoding];
+				}
+				// we want output encoding to be a JS Object, where
+				// key = sourceEncoding's character code and
+				// value = outputEncoding's character code.
+				if (outputEncoding) {
+					isUnicode = false;
+					newtext = [];
+					for (i = 0, l = text.length; i < l; i++) {
+						ch = outputEncoding[text.charCodeAt(i)];
+						if (ch) {
+							newtext.push(
+								String.fromCharCode(ch));
+						} else {
+							newtext.push(
+								text[i]);
+						}
+
+						// since we are looping over chars anyway, might as well
+						// check for residual unicodeness
+						if (newtext[i].charCodeAt(0) >> 8) {
+							/* more than 255 */
+							isUnicode = true;
+						}
+					}
+					text = newtext.join('');
+				}
+			}
+
+			i = text.length;
+			// isUnicode may be set to false above. Hence the triple-equal to undefined
+			while (isUnicode === undefined && i !== 0) {
+				if (text.charCodeAt(i - 1) >> 8) {
+					/* more than 255 */
+					isUnicode = true;
+				}
+				i--;
+			}
+			if (!isUnicode) {
+				return text;
+			}
+
+			newtext = flags.noBOM ? [] : [254, 255];
+			for (i = 0, l = text.length; i < l; i++) {
+				ch = text.charCodeAt(i);
+				bch = ch >> 8; // divide by 256
+				if (bch >> 8) {
+					/* something left after dividing by 256 second time */
+					throw new Error("Character at position " + i + " of string '"
+						+ text + "' exceeds 16bits. Cannot be encoded into UCS-2 BE");
+				}
+				newtext.push(bch);
+				newtext.push(ch - (bch << 8));
+			}
+			return String.fromCharCode.apply(undefined, newtext);
+		},
+		pdfEscape = function(text, flags) {
+			/**
+			 * Replace '/', '(', and ')' with pdf-safe versions
+			 *
+			 * Doing to8bitStream does NOT make this PDF display unicode text. For that
+			 * we also need to reference a unicode font and embed it - royal pain in the rear.
+			 *
+			 * There is still a benefit to to8bitStream - PDF simply cannot handle 16bit chars,
+			 * which JavaScript Strings are happy to provide. So, while we still cannot display
+			 * 2-byte characters property, at least CONDITIONALLY converting (entire string containing)
+			 * 16bit chars to (USC-2-BE) 2-bytes per char + BOM streams we ensure that entire PDF
+			 * is still parseable.
+			 * This will allow immediate support for unicode in document properties strings.
+			 */
+			return to8bitStream(text, flags).replace(/\\/g, '\\\\').replace(/\(/g, '\\(').replace(/\)/g, '\\)');
+		},
+		putInfo = function() {
+			out('/Producer (jsPDF ' + jsPDF.version + ')');
+			for(var key in documentProperties) {
+				if(documentProperties.hasOwnProperty(key) && documentProperties[key]) {
+					out('/'+key.substr(0,1).toUpperCase() + key.substr(1)
+						+' (' + pdfEscape(documentProperties[key]) + ')');
+				}
+			}
+			var created  = new Date(),
+				tzoffset = created.getTimezoneOffset(),
+				tzsign   = tzoffset < 0 ? '+' : '-',
+				tzhour   = Math.floor(Math.abs(tzoffset / 60)),
+				tzmin    = Math.abs(tzoffset % 60),
+				tzstr    = [tzsign, padd2(tzhour), "'", padd2(tzmin), "'"].join('');
+			out(['/CreationDate (D:',
+					created.getFullYear(),
+					padd2(created.getMonth() + 1),
+					padd2(created.getDate()),
+					padd2(created.getHours()),
+					padd2(created.getMinutes()),
+					padd2(created.getSeconds()), tzstr, ')'].join(''));
+		},
+		putCatalog = function() {
+			out('/Type /Catalog');
+			out('/Pages 1 0 R');
+			// PDF13ref Section 7.2.1
+			if (!zoomMode) zoomMode = 'fullwidth';
+			switch(zoomMode) {
+				case 'fullwidth'  : out('/OpenAction [3 0 R /FitH null]');       break;
+				case 'fullheight' : out('/OpenAction [3 0 R /FitV null]');       break;
+				case 'fullpage'   : out('/OpenAction [3 0 R /Fit]');             break;
+				case 'original'   : out('/OpenAction [3 0 R /XYZ null null 1]'); break;
+				default:
+					var pcn = '' + zoomMode;
+					if (pcn.substr(pcn.length-1) === '%')
+						zoomMode = parseInt(zoomMode) / 100;
+					if (typeof zoomMode === 'number') {
+						out('/OpenAction [3 0 R /XYZ null null '+f2(zoomMode)+']');
+					}
+			}
+			if (!layoutMode) layoutMode = 'continuous';
+			switch(layoutMode) {
+				case 'continuous' : out('/PageLayout /OneColumn');      break;
+				case 'single'     : out('/PageLayout /SinglePage');     break;
+				case 'two':
+				case 'twoleft'    : out('/PageLayout /TwoColumnLeft');  break;
+				case 'tworight'   : out('/PageLayout /TwoColumnRight'); break;
+			}
+			if (pageMode) {
+				/**
+				 * A name object specifying how the document should be displayed when opened:
+				 * UseNone      : Neither document outline nor thumbnail images visible -- DEFAULT
+				 * UseOutlines  : Document outline visible
+				 * UseThumbs    : Thumbnail images visible
+				 * FullScreen   : Full-screen mode, with no menu bar, window controls, or any other window visible
+				 */
+				out('/PageMode /' + pageMode);
+			}
+			events.publish('putCatalog');
+		},
+		putTrailer = function() {
+			out('/Size ' + (objectNumber + 1));
+			out('/Root ' + objectNumber + ' 0 R');
+			out('/Info ' + (objectNumber - 1) + ' 0 R');
+		},
+		beginPage = function(width,height) {
+			// Dimensions are stored as user units and converted to points on output
+			var orientation = typeof height === 'string' && height.toLowerCase();
+			if (typeof width === 'string') {
+				var format = width.toLowerCase();
+				if (pageFormats.hasOwnProperty(format)) {
+					width  = pageFormats[format][0] / k;
+					height = pageFormats[format][1] / k;
+				}
+			}
+			if (Array.isArray(width)) {
+				height = width[1];
+				width = width[0];
+			}
+			if (orientation) {
+				switch(orientation.substr(0,1)) {
+					case 'l': if (height > width ) orientation = 's'; break;
+					case 'p': if (width > height ) orientation = 's'; break;
+				}
+				if (orientation === 's') { tmp = width; width = height; height = tmp; }
+			}
+			outToPages = true;
+			pages[++page] = [];
+			pagedim[page] = {
+				width  : Number(width)  || pageWidth,
+				height : Number(height) || pageHeight
+			};
+			_setPage(page);
+		},
+		_addPage = function() {
+			beginPage.apply(this, arguments);
+			// Set line width
+			out(f2(lineWidth * k) + ' w');
+			// Set draw color
+			out(drawColor);
+			// resurrecting non-default line caps, joins
+			if (lineCapID !== 0) {
+				out(lineCapID + ' J');
+			}
+			if (lineJoinID !== 0) {
+				out(lineJoinID + ' j');
+			}
+			events.publish('addPage', { pageNumber : page });
+		},
+		_setPage = function(n) {
+			if (n > 0 && n <= page) {
+				currentPage = n;
+				pageWidth = pagedim[n].width;
+				pageHeight = pagedim[n].height;
+			}
+		},
+		/**
+		 * Returns a document-specific font key - a label assigned to a
+		 * font name + font type combination at the time the font was added
+		 * to the font inventory.
+		 *
+		 * Font key is used as label for the desired font for a block of text
+		 * to be added to the PDF document stream.
+		 * @private
+		 * @function
+		 * @param fontName {String} can be undefined on "falthy" to indicate "use current"
+		 * @param fontStyle {String} can be undefined on "falthy" to indicate "use current"
+		 * @returns {String} Font key.
+		 */
+		getFont = function(fontName, fontStyle) {
+			var key;
+
+			fontName  = fontName  !== undefined ? fontName  : fonts[activeFontKey].fontName;
+			fontStyle = fontStyle !== undefined ? fontStyle : fonts[activeFontKey].fontStyle;
+
+			try {
+			 // get a string like 'F3' - the KEY corresponding tot he font + type combination.
+				key = fontmap[fontName][fontStyle];
+			} catch (e) {}
+
+			if (!key) {
+				throw new Error("Unable to look up font label for font '" + fontName + "', '"
+					+ fontStyle + "'. Refer to getFontList() for available fonts.");
+			}
+			return key;
+		},
+		buildDocument = function() {
+
+			outToPages = false; // switches out() to content
+			objectNumber = 2;
+			content = [];
+			offsets = [];
+
+			// putHeader()
+			out('%PDF-' + pdfVersion);
+
+			putPages();
+
+			putResources();
+
+			// Info
+			newObject();
+			out('<<');
+			putInfo();
+			out('>>');
+			out('endobj');
+
+			// Catalog
+			newObject();
+			out('<<');
+			putCatalog();
+			out('>>');
+			out('endobj');
+
+			// Cross-ref
+			var o = content_length, i, p = "0000000000";
+			out('xref');
+			out('0 ' + (objectNumber + 1));
+			out(p+' 65535 f ');
+			for (i = 1; i <= objectNumber; i++) {
+				out((p + offsets[i]).slice(-10) + ' 00000 n ');
+			}
+			// Trailer
+			out('trailer');
+			out('<<');
+			putTrailer();
+			out('>>');
+			out('startxref');
+			out(o);
+			out('%%EOF');
+
+			outToPages = true;
+
+			return content.join('\n');
+		},
+		getStyle = function(style) {
+			// see path-painting operators in PDF spec
+			var op = 'S'; // stroke
+			if (style === 'F') {
+				op = 'f'; // fill
+			} else if (style === 'FD' || style === 'DF') {
+				op = 'B'; // both
+			} else if (style === 'f' || style === 'f*' || style === 'B' || style === 'B*') {
+				/*
+				Allow direct use of these PDF path-painting operators:
+				- f	fill using nonzero winding number rule
+				- f*	fill using even-odd rule
+				- B	fill then stroke with fill using non-zero winding number rule
+				- B*	fill then stroke with fill using even-odd rule
+				*/
+				op = style;
+			}
+			return op;
+		},
+		getArrayBuffer = function() {
+			var data = buildDocument(), len = data.length,
+				ab = new ArrayBuffer(len), u8 = new Uint8Array(ab);
+
+			while(len--) u8[len] = data.charCodeAt(len);
+			return ab;
+		},
+		getBlob = function() {
+			return new Blob([getArrayBuffer()], { type : "application/pdf" });
+		},
+		/**
+		 * Generates the PDF document.
+		 *
+		 * If `type` argument is undefined, output is raw body of resulting PDF returned as a string.
+		 *
+		 * @param {String} type A string identifying one of the possible output types.
+		 * @param {Object} options An object providing some additional signalling to PDF generator.
+		 * @function
+		 * @returns {jsPDF}
+		 * @methodOf jsPDF#
+		 * @name output
+		 */
+		output = SAFE(function(type, options) {
+			var datauri = ('' + type).substr(0,6) === 'dataur'
+				? 'data:application/pdf;base64,'+btoa(buildDocument()):0;
+
+			switch (type) {
+				case undefined:
+					return buildDocument();
+				case 'save':
+					if (navigator.getUserMedia) {
+						if (global.URL === undefined
+						|| global.URL.createObjectURL === undefined) {
+							return API.output('dataurlnewwindow');
+						}
+					}
+					saveAs(getBlob(), options);
+					if(typeof saveAs.unload === 'function') {
+						if(global.setTimeout) {
+							setTimeout(saveAs.unload,911);
+						}
+					}
+					break;
+				case 'arraybuffer':
+					return getArrayBuffer();
+				case 'blob':
+					return getBlob();
+				case 'bloburi':
+				case 'bloburl':
+					// User is responsible of calling revokeObjectURL
+					return global.URL && global.URL.createObjectURL(getBlob()) || void 0;
+				case 'datauristring':
+				case 'dataurlstring':
+					return datauri;
+				case 'dataurlnewwindow':
+					var nW = global.open(datauri);
+					if (nW || typeof safari === "undefined") return nW;
+					/* pass through */
+				case 'datauri':
+				case 'dataurl':
+					return global.document.location.href = datauri;
+				default:
+					throw new Error('Output type "' + type + '" is not supported.');
+			}
+			// @TODO: Add different output options
+		});
+
+		switch (unit) {
+			case 'pt':  k = 1;          break;
+			case 'mm':  k = 72 / 25.4;  break;
+			case 'cm':  k = 72 / 2.54;  break;
+			case 'in':  k = 72;         break;
+			case 'px':  k = 96 / 72;    break;
+			case 'pc':  k = 12;         break;
+			case 'em':  k = 12;         break;
+			case 'ex':  k = 6;          break;
+			default:
+				throw ('Invalid unit: ' + unit);
+		}
+
+		//---------------------------------------
+		// Public API
+
+		/**
+		 * Object exposing internal API to plugins
+		 * @public
+		 */
+		API.internal = {
+			'pdfEscape' : pdfEscape,
+			'getStyle' : getStyle,
+			/**
+			 * Returns {FontObject} describing a particular font.
+			 * @public
+			 * @function
+			 * @param fontName {String} (Optional) Font's family name
+			 * @param fontStyle {String} (Optional) Font's style variation name (Example:"Italic")
+			 * @returns {FontObject}
+			 */
+			'getFont' : function() {
+				return fonts[getFont.apply(API, arguments)];
+			},
+			'getFontSize' : function() {
+				return activeFontSize;
+			},
+			'getLineHeight' : function() {
+				return activeFontSize * lineHeightProportion;
+			},
+			'write' : function(string1 /*, string2, string3, etc */) {
+				out(arguments.length === 1 ? string1 : Array.prototype.join.call(arguments, ' '));
+			},
+			'getCoordinateString' : function(value) {
+				return f2(value * k);
+			},
+			'getVerticalCoordinateString' : function(value) {
+				return f2((pageHeight - value) * k);
+			},
+			'collections' : {},
+			'newObject' : newObject,
+			'putStream' : putStream,
+			'events' : events,
+			// ratio that you use in multiplication of a given "size" number to arrive to 'point'
+			// units of measurement.
+			// scaleFactor is set at initialization of the document and calculated against the stated
+			// default measurement units for the document.
+			// If default is "mm", k is the number that will turn number in 'mm' into 'points' number.
+			// through multiplication.
+			'scaleFactor' : k,
+			'pageSize' : {
+				get width() {
+					return pageWidth
+				},
+				get height() {
+					return pageHeight
+				}
+			},
+			'output' : function(type, options) {
+				return output(type, options);
+			},
+			'getNumberOfPages' : function() {
+				return pages.length - 1;
+			},
+			'pages' : pages
+		};
+
+		/**
+		 * Adds (and transfers the focus to) new page to the PDF document.
+		 * @function
+		 * @returns {jsPDF}
+		 *
+		 * @methodOf jsPDF#
+		 * @name addPage
+		 */
+		API.addPage = function() {
+			_addPage.apply(this, arguments);
+			return this;
+		};
+		API.setPage = function() {
+			_setPage.apply(this, arguments);
+			return this;
+		};
+		API.setDisplayMode = function(zoom, layout, pmode) {
+			zoomMode   = zoom;
+			layoutMode = layout;
+			pageMode   = pmode;
+			return this;
+		},
+
+		/**
+		 * Adds text to page. Supports adding multiline text when 'text' argument is an Array of Strings.
+		 *
+		 * @function
+		 * @param {String|Array} text String or array of strings to be added to the page. Each line is shifted one line down per font, spacing settings declared before this call.
+		 * @param {Number} x Coordinate (in units declared at inception of PDF document) against left edge of the page
+		 * @param {Number} y Coordinate (in units declared at inception of PDF document) against upper edge of the page
+		 * @param {Object} flags Collection of settings signalling how the text must be encoded. Defaults are sane. If you think you want to pass some flags, you likely can read the source.
+		 * @returns {jsPDF}
+		 * @methodOf jsPDF#
+		 * @name text
+		 */
+		API.text = function(text, x, y, flags, angle) {
+			/**
+			 * Inserts something like this into PDF
+			 *   BT
+			 *    /F1 16 Tf  % Font name + size
+			 *    16 TL % How many units down for next line in multiline text
+			 *    0 g % color
+			 *    28.35 813.54 Td % position
+			 *    (line one) Tj
+			 *    T* (line two) Tj
+			 *    T* (line three) Tj
+			 *   ET
+			 */
+			function ESC(s) {
+				s = s.split("\t").join(Array(options.TabLen||9).join(" "));
+				return pdfEscape(s, flags);
+			}
+
+			// Pre-August-2012 the order of arguments was function(x, y, text, flags)
+			// in effort to make all calls have similar signature like
+			//   function(data, coordinates... , miscellaneous)
+			// this method had its args flipped.
+			// code below allows backward compatibility with old arg order.
+			if (typeof text === 'number') {
+				tmp = y;
+				y = x;
+				x = text;
+				text = tmp;
+			}
+
+			// If there are any newlines in text, we assume
+			// the user wanted to print multiple lines, so break the
+			// text up into an array.  If the text is already an array,
+			// we assume the user knows what they are doing.
+			if (typeof text === 'string' && text.match(/[\n\r]/)) {
+				text = text.split(/\r\n|\r|\n/g);
+			}
+			if (typeof flags === 'number') {
+				angle = flags;
+				flags = null;
+			}
+			var xtra = '',mode = 'Td', todo;
+			if (angle) {
+				angle *= (Math.PI / 180);
+				var c = Math.cos(angle),
+				s = Math.sin(angle);
+				xtra = [f2(c), f2(s), f2(s * -1), f2(c), ''].join(" ");
+				mode = 'Tm';
+			}
+			flags = flags || {};
+			if (!('noBOM' in flags))
+				flags.noBOM = true;
+			if (!('autoencode' in flags))
+				flags.autoencode = true;
+
+			if (typeof text === 'string') {
+				text = ESC(text);
+			} else if (text instanceof Array) {
+				// we don't want to destroy  original text array, so cloning it
+				var sa = text.concat(), da = [], len = sa.length;
+				// we do array.join('text that must not be PDFescaped")
+				// thus, pdfEscape each component separately
+				while (len--) {
+					da.push(ESC(sa.shift()));
+				}
+				var linesLeft = Math.ceil((pageHeight - y) * k / (activeFontSize * lineHeightProportion));
+				if (0 <= linesLeft && linesLeft < da.length + 1) {
+					todo = da.splice(linesLeft-1);
+				}
+				text = da.join(") Tj\nT* (");
+			} else {
+				throw new Error('Type of text must be string or Array. "' + text + '" is not recognized.');
+			}
+			// Using "'" ("go next line and render text" mark) would save space but would complicate our rendering code, templates
+
+			// BT .. ET does NOT have default settings for Tf. You must state that explicitely every time for BT .. ET
+			// if you want text transformation matrix (+ multiline) to work reliably (which reads sizes of things from font declarations)
+			// Thus, there is NO useful, *reliable* concept of "default" font for a page.
+			// The fact that "default" (reuse font used before) font worked before in basic cases is an accident
+			// - readers dealing smartly with brokenness of jsPDF's markup.
+			out(
+				'BT\n/' +
+				activeFontKey + ' ' + activeFontSize + ' Tf\n' +     // font face, style, size
+				(activeFontSize * lineHeightProportion) + ' TL\n' +  // line spacing
+				textColor +
+				'\n' + xtra + f2(x * k) + ' ' + f2((pageHeight - y) * k) + ' ' + mode + '\n(' +
+				text +
+				') Tj\nET');
+
+			if (todo) {
+				this.addPage();
+				this.text( todo, x, activeFontSize * 1.7 / k);
+			}
+
+			return this;
+		};
+
+		API.lstext = function(text, x, y, spacing) {
+			for (var i = 0, len = text.length ; i < len; i++, x += spacing) this.text(text[i], x, y);
+		};
+
+		API.line = function(x1, y1, x2, y2) {
+			return this.lines([[x2 - x1, y2 - y1]], x1, y1);
+		};
+
+		API.clip = function() {
+			// By patrick-roberts, github.com/MrRio/jsPDF/issues/328
+			// Call .clip() after calling .rect() with a style argument of null
+			out('W') // clip
+			out('S') // stroke path; necessary for clip to work
+		};
+
+		/**
+		 * Adds series of curves (straight lines or cubic bezier curves) to canvas, starting at `x`, `y` coordinates.
+		 * All data points in `lines` are relative to last line origin.
+		 * `x`, `y` become x1,y1 for first line / curve in the set.
+		 * For lines you only need to specify [x2, y2] - (ending point) vector against x1, y1 starting point.
+		 * For bezier curves you need to specify [x2,y2,x3,y3,x4,y4] - vectors to control points 1, 2, ending point. All vectors are against the start of the curve - x1,y1.
+		 *
+		 * @example .lines([[2,2],[-2,2],[1,1,2,2,3,3],[2,1]], 212,110, 10) // line, line, bezier curve, line
+		 * @param {Array} lines Array of *vector* shifts as pairs (lines) or sextets (cubic bezier curves).
+		 * @param {Number} x Coordinate (in units declared at inception of PDF document) against left edge of the page
+		 * @param {Number} y Coordinate (in units declared at inception of PDF document) against upper edge of the page
+		 * @param {Number} scale (Defaults to [1.0,1.0]) x,y Scaling factor for all vectors. Elements can be any floating number Sub-one makes drawing smaller. Over-one grows the drawing. Negative flips the direction.
+		 * @param {String} style A string specifying the painting style or null.  Valid styles include: 'S' [default] - stroke, 'F' - fill,  and 'DF' (or 'FD') -  fill then stroke. A null value postpones setting the style so that a shape may be composed using multiple method calls. The last drawing method call used to define the shape should not have a null style argument.
+		 * @param {Boolean} closed If true, the path is closed with a straight line from the end of the last curve to the starting point.
+		 * @function
+		 * @returns {jsPDF}
+		 * @methodOf jsPDF#
+		 * @name lines
+		 */
+		API.lines = function(lines, x, y, scale, style, closed) {
+			var scalex,scaley,i,l,leg,x2,y2,x3,y3,x4,y4;
+
+			// Pre-August-2012 the order of arguments was function(x, y, lines, scale, style)
+			// in effort to make all calls have similar signature like
+			//   function(content, coordinateX, coordinateY , miscellaneous)
+			// this method had its args flipped.
+			// code below allows backward compatibility with old arg order.
+			if (typeof lines === 'number') {
+				tmp = y;
+				y = x;
+				x = lines;
+				lines = tmp;
+			}
+
+			scale = scale || [1, 1];
+
+			// starting point
+			out(f3(x * k) + ' ' + f3((pageHeight - y) * k) + ' m ');
+
+			scalex = scale[0];
+			scaley = scale[1];
+			l = lines.length;
+			//, x2, y2 // bezier only. In page default measurement "units", *after* scaling
+			//, x3, y3 // bezier only. In page default measurement "units", *after* scaling
+			// ending point for all, lines and bezier. . In page default measurement "units", *after* scaling
+			x4 = x; // last / ending point = starting point for first item.
+			y4 = y; // last / ending point = starting point for first item.
+
+			for (i = 0; i < l; i++) {
+				leg = lines[i];
+				if (leg.length === 2) {
+					// simple line
+					x4 = leg[0] * scalex + x4; // here last x4 was prior ending point
+					y4 = leg[1] * scaley + y4; // here last y4 was prior ending point
+					out(f3(x4 * k) + ' ' + f3((pageHeight - y4) * k) + ' l');
+				} else {
+					// bezier curve
+					x2 = leg[0] * scalex + x4; // here last x4 is prior ending point
+					y2 = leg[1] * scaley + y4; // here last y4 is prior ending point
+					x3 = leg[2] * scalex + x4; // here last x4 is prior ending point
+					y3 = leg[3] * scaley + y4; // here last y4 is prior ending point
+					x4 = leg[4] * scalex + x4; // here last x4 was prior ending point
+					y4 = leg[5] * scaley + y4; // here last y4 was prior ending point
+					out(
+						f3(x2 * k) + ' ' +
+						f3((pageHeight - y2) * k) + ' ' +
+						f3(x3 * k) + ' ' +
+						f3((pageHeight - y3) * k) + ' ' +
+						f3(x4 * k) + ' ' +
+						f3((pageHeight - y4) * k) + ' c');
+				}
+			}
+
+			if (closed) {
+				out(' h');
+			}
+
+			// stroking / filling / both the path
+			if (style !== null) {
+				out(getStyle(style));
+			}
+			return this;
+		};
+
+		/**
+		 * Adds a rectangle to PDF
+		 *
+		 * @param {Number} x Coordinate (in units declared at inception of PDF document) against left edge of the page
+		 * @param {Number} y Coordinate (in units declared at inception of PDF document) against upper edge of the page
+		 * @param {Number} w Width (in units declared at inception of PDF document)
+		 * @param {Number} h Height (in units declared at inception of PDF document)
+		 * @param {String} style A string specifying the painting style or null.  Valid styles include: 'S' [default] - stroke, 'F' - fill,  and 'DF' (or 'FD') -  fill then stroke. A null value postpones setting the style so that a shape may be composed using multiple method calls. The last drawing method call used to define the shape should not have a null style argument.
+		 * @function
+		 * @returns {jsPDF}
+		 * @methodOf jsPDF#
+		 * @name rect
+		 */
+		API.rect = function(x, y, w, h, style) {
+			var op = getStyle(style);
+			out([
+					f2(x * k),
+					f2((pageHeight - y) * k),
+					f2(w * k),
+					f2(-h * k),
+					're'
+				].join(' '));
+
+			if (style !== null) {
+				out(getStyle(style));
+			}
+
+			return this;
+		};
+
+		/**
+		 * Adds a triangle to PDF
+		 *
+		 * @param {Number} x1 Coordinate (in units declared at inception of PDF document) against left edge of the page
+		 * @param {Number} y1 Coordinate (in units declared at inception of PDF document) against upper edge of the page
+		 * @param {Number} x2 Coordinate (in units declared at inception of PDF document) against left edge of the page
+		 * @param {Number} y2 Coordinate (in units declared at inception of PDF document) against upper edge of the page
+		 * @param {Number} x3 Coordinate (in units declared at inception of PDF document) against left edge of the page
+		 * @param {Number} y3 Coordinate (in units declared at inception of PDF document) against upper edge of the page
+		 * @param {String} style A string specifying the painting style or null.  Valid styles include: 'S' [default] - stroke, 'F' - fill,  and 'DF' (or 'FD') -  fill then stroke. A null value postpones setting the style so that a shape may be composed using multiple method calls. The last drawing method call used to define the shape should not have a null style argument.
+		 * @function
+		 * @returns {jsPDF}
+		 * @methodOf jsPDF#
+		 * @name triangle
+		 */
+		API.triangle = function(x1, y1, x2, y2, x3, y3, style) {
+			this.lines(
+				[
+					[x2 - x1, y2 - y1], // vector to point 2
+					[x3 - x2, y3 - y2], // vector to point 3
+					[x1 - x3, y1 - y3]// closing vector back to point 1
+				],
+				x1,
+				y1, // start of path
+				[1, 1],
+				style,
+				true);
+			return this;
+		};
+
+		/**
+		 * Adds a rectangle with rounded corners to PDF
+		 *
+		 * @param {Number} x Coordinate (in units declared at inception of PDF document) against left edge of the page
+		 * @param {Number} y Coordinate (in units declared at inception of PDF document) against upper edge of the page
+		 * @param {Number} w Width (in units declared at inception of PDF document)
+		 * @param {Number} h Height (in units declared at inception of PDF document)
+		 * @param {Number} rx Radius along x axis (in units declared at inception of PDF document)
+		 * @param {Number} rx Radius along y axis (in units declared at inception of PDF document)
+		 * @param {String} style A string specifying the painting style or null.  Valid styles include: 'S' [default] - stroke, 'F' - fill,  and 'DF' (or 'FD') -  fill then stroke. A null value postpones setting the style so that a shape may be composed using multiple method calls. The last drawing method call used to define the shape should not have a null style argument.
+		 * @function
+		 * @returns {jsPDF}
+		 * @methodOf jsPDF#
+		 * @name roundedRect
+		 */
+		API.roundedRect = function(x, y, w, h, rx, ry, style) {
+			var MyArc = 4 / 3 * (Math.SQRT2 - 1);
+			this.lines(
+				[
+					[(w - 2 * rx), 0],
+					[(rx * MyArc), 0, rx, ry - (ry * MyArc), rx, ry],
+					[0, (h - 2 * ry)],
+					[0, (ry * MyArc),  - (rx * MyArc), ry, -rx, ry],
+					[(-w + 2 * rx), 0],
+					[ - (rx * MyArc), 0, -rx,  - (ry * MyArc), -rx, -ry],
+					[0, (-h + 2 * ry)],
+					[0,  - (ry * MyArc), (rx * MyArc), -ry, rx, -ry]
+				],
+				x + rx,
+				y, // start of path
+				[1, 1],
+				style);
+			return this;
+		};
+
+		/**
+		 * Adds an ellipse to PDF
+		 *
+		 * @param {Number} x Coordinate (in units declared at inception of PDF document) against left edge of the page
+		 * @param {Number} y Coordinate (in units declared at inception of PDF document) against upper edge of the page
+		 * @param {Number} rx Radius along x axis (in units declared at inception of PDF document)
+		 * @param {Number} rx Radius along y axis (in units declared at inception of PDF document)
+		 * @param {String} style A string specifying the painting style or null.  Valid styles include: 'S' [default] - stroke, 'F' - fill,  and 'DF' (or 'FD') -  fill then stroke. A null value postpones setting the style so that a shape may be composed using multiple method calls. The last drawing method call used to define the shape should not have a null style argument.
+		 * @function
+		 * @returns {jsPDF}
+		 * @methodOf jsPDF#
+		 * @name ellipse
+		 */
+		API.ellipse = function(x, y, rx, ry, style) {
+			var lx = 4 / 3 * (Math.SQRT2 - 1) * rx,
+				ly = 4 / 3 * (Math.SQRT2 - 1) * ry;
+
+			out([
+					f2((x + rx) * k),
+					f2((pageHeight - y) * k),
+					'm',
+					f2((x + rx) * k),
+					f2((pageHeight - (y - ly)) * k),
+					f2((x + lx) * k),
+					f2((pageHeight - (y - ry)) * k),
+					f2(x * k),
+					f2((pageHeight - (y - ry)) * k),
+					'c'
+				].join(' '));
+			out([
+					f2((x - lx) * k),
+					f2((pageHeight - (y - ry)) * k),
+					f2((x - rx) * k),
+					f2((pageHeight - (y - ly)) * k),
+					f2((x - rx) * k),
+					f2((pageHeight - y) * k),
+					'c'
+				].join(' '));
+			out([
+					f2((x - rx) * k),
+					f2((pageHeight - (y + ly)) * k),
+					f2((x - lx) * k),
+					f2((pageHeight - (y + ry)) * k),
+					f2(x * k),
+					f2((pageHeight - (y + ry)) * k),
+					'c'
+				].join(' '));
+			out([
+					f2((x + lx) * k),
+					f2((pageHeight - (y + ry)) * k),
+					f2((x + rx) * k),
+					f2((pageHeight - (y + ly)) * k),
+					f2((x + rx) * k),
+					f2((pageHeight - y) * k),
+					'c'
+				].join(' '));
+
+			if (style !== null) {
+				out(getStyle(style));
+			}
+
+			return this;
+		};
+
+		/**
+		 * Adds an circle to PDF
+		 *
+		 * @param {Number} x Coordinate (in units declared at inception of PDF document) against left edge of the page
+		 * @param {Number} y Coordinate (in units declared at inception of PDF document) against upper edge of the page
+		 * @param {Number} r Radius (in units declared at inception of PDF document)
+		 * @param {String} style A string specifying the painting style or null.  Valid styles include: 'S' [default] - stroke, 'F' - fill,  and 'DF' (or 'FD') -  fill then stroke. A null value postpones setting the style so that a shape may be composed using multiple method calls. The last drawing method call used to define the shape should not have a null style argument.
+		 * @function
+		 * @returns {jsPDF}
+		 * @methodOf jsPDF#
+		 * @name circle
+		 */
+		API.circle = function(x, y, r, style) {
+			return this.ellipse(x, y, r, r, style);
+		};
+
+		/**
+		 * Adds a properties to the PDF document
+		 *
+		 * @param {Object} A property_name-to-property_value object structure.
+		 * @function
+		 * @returns {jsPDF}
+		 * @methodOf jsPDF#
+		 * @name setProperties
+		 */
+		API.setProperties = function(properties) {
+			// copying only those properties we can render.
+			for (var property in documentProperties) {
+				if (documentProperties.hasOwnProperty(property) && properties[property]) {
+					documentProperties[property] = properties[property];
+				}
+			}
+			return this;
+		};
+
+		/**
+		 * Sets font size for upcoming text elements.
+		 *
+		 * @param {Number} size Font size in points.
+		 * @function
+		 * @returns {jsPDF}
+		 * @methodOf jsPDF#
+		 * @name setFontSize
+		 */
+		API.setFontSize = function(size) {
+			activeFontSize = size;
+			return this;
+		};
+
+		/**
+		 * Sets text font face, variant for upcoming text elements.
+		 * See output of jsPDF.getFontList() for possible font names, styles.
+		 *
+		 * @param {String} fontName Font name or family. Example: "times"
+		 * @param {String} fontStyle Font style or variant. Example: "italic"
+		 * @function
+		 * @returns {jsPDF}
+		 * @methodOf jsPDF#
+		 * @name setFont
+		 */
+		API.setFont = function(fontName, fontStyle) {
+			activeFontKey = getFont(fontName, fontStyle);
+			// if font is not found, the above line blows up and we never go further
+			return this;
+		};
+
+		/**
+		 * Switches font style or variant for upcoming text elements,
+		 * while keeping the font face or family same.
+		 * See output of jsPDF.getFontList() for possible font names, styles.
+		 *
+		 * @param {String} style Font style or variant. Example: "italic"
+		 * @function
+		 * @returns {jsPDF}
+		 * @methodOf jsPDF#
+		 * @name setFontStyle
+		 */
+		API.setFontStyle = API.setFontType = function(style) {
+			activeFontKey = getFont(undefined, style);
+			// if font is not found, the above line blows up and we never go further
+			return this;
+		};
+
+		/**
+		 * Returns an object - a tree of fontName to fontStyle relationships available to
+		 * active PDF document.
+		 *
+		 * @public
+		 * @function
+		 * @returns {Object} Like {'times':['normal', 'italic', ... ], 'arial':['normal', 'bold', ... ], ... }
+		 * @methodOf jsPDF#
+		 * @name getFontList
+		 */
+		API.getFontList = function() {
+			// TODO: iterate over fonts array or return copy of fontmap instead in case more are ever added.
+			var list = {},fontName,fontStyle,tmp;
+
+			for (fontName in fontmap) {
+				if (fontmap.hasOwnProperty(fontName)) {
+					list[fontName] = tmp = [];
+					for (fontStyle in fontmap[fontName]) {
+						if (fontmap[fontName].hasOwnProperty(fontStyle)) {
+							tmp.push(fontStyle);
+						}
+					}
+				}
+			}
+
+			return list;
+		};
+
+		/**
+		 * Sets line width for upcoming lines.
+		 *
+		 * @param {Number} width Line width (in units declared at inception of PDF document)
+		 * @function
+		 * @returns {jsPDF}
+		 * @methodOf jsPDF#
+		 * @name setLineWidth
+		 */
+		API.setLineWidth = function(width) {
+			out((width * k).toFixed(2) + ' w');
+			return this;
+		};
+
+		/**
+		 * Sets the stroke color for upcoming elements.
+		 *
+		 * Depending on the number of arguments given, Gray, RGB, or CMYK
+		 * color space is implied.
+		 *
+		 * When only ch1 is given, "Gray" color space is implied and it
+		 * must be a value in the range from 0.00 (solid black) to to 1.00 (white)
+		 * if values are communicated as String types, or in range from 0 (black)
+		 * to 255 (white) if communicated as Number type.
+		 * The RGB-like 0-255 range is provided for backward compatibility.
+		 *
+		 * When only ch1,ch2,ch3 are given, "RGB" color space is implied and each
+		 * value must be in the range from 0.00 (minimum intensity) to to 1.00
+		 * (max intensity) if values are communicated as String types, or
+		 * from 0 (min intensity) to to 255 (max intensity) if values are communicated
+		 * as Number types.
+		 * The RGB-like 0-255 range is provided for backward compatibility.
+		 *
+		 * When ch1,ch2,ch3,ch4 are given, "CMYK" color space is implied and each
+		 * value must be a in the range from 0.00 (0% concentration) to to
+		 * 1.00 (100% concentration)
+		 *
+		 * Because JavaScript treats fixed point numbers badly (rounds to
+		 * floating point nearest to binary representation) it is highly advised to
+		 * communicate the fractional numbers as String types, not JavaScript Number type.
+		 *
+		 * @param {Number|String} ch1 Color channel value
+		 * @param {Number|String} ch2 Color channel value
+		 * @param {Number|String} ch3 Color channel value
+		 * @param {Number|String} ch4 Color channel value
+		 *
+		 * @function
+		 * @returns {jsPDF}
+		 * @methodOf jsPDF#
+		 * @name setDrawColor
+		 */
+		API.setDrawColor = function(ch1, ch2, ch3, ch4) {
+			var color;
+			if (ch2 === undefined || (ch4 === undefined && ch1 === ch2 === ch3)) {
+				// Gray color space.
+				if (typeof ch1 === 'string') {
+					color = ch1 + ' G';
+				} else {
+					color = f2(ch1 / 255) + ' G';
+				}
+			} else if (ch4 === undefined) {
+				// RGB
+				if (typeof ch1 === 'string') {
+					color = [ch1, ch2, ch3, 'RG'].join(' ');
+				} else {
+					color = [f2(ch1 / 255), f2(ch2 / 255), f2(ch3 / 255), 'RG'].join(' ');
+				}
+			} else {
+				// CMYK
+				if (typeof ch1 === 'string') {
+					color = [ch1, ch2, ch3, ch4, 'K'].join(' ');
+				} else {
+					color = [f2(ch1), f2(ch2), f2(ch3), f2(ch4), 'K'].join(' ');
+				}
+			}
+
+			out(color);
+			return this;
+		};
+
+		/**
+		 * Sets the fill color for upcoming elements.
+		 *
+		 * Depending on the number of arguments given, Gray, RGB, or CMYK
+		 * color space is implied.
+		 *
+		 * When only ch1 is given, "Gray" color space is implied and it
+		 * must be a value in the range from 0.00 (solid black) to to 1.00 (white)
+		 * if values are communicated as String types, or in range from 0 (black)
+		 * to 255 (white) if communicated as Number type.
+		 * The RGB-like 0-255 range is provided for backward compatibility.
+		 *
+		 * When only ch1,ch2,ch3 are given, "RGB" color space is implied and each
+		 * value must be in the range from 0.00 (minimum intensity) to to 1.00
+		 * (max intensity) if values are communicated as String types, or
+		 * from 0 (min intensity) to to 255 (max intensity) if values are communicated
+		 * as Number types.
+		 * The RGB-like 0-255 range is provided for backward compatibility.
+		 *
+		 * When ch1,ch2,ch3,ch4 are given, "CMYK" color space is implied and each
+		 * value must be a in the range from 0.00 (0% concentration) to to
+		 * 1.00 (100% concentration)
+		 *
+		 * Because JavaScript treats fixed point numbers badly (rounds to
+		 * floating point nearest to binary representation) it is highly advised to
+		 * communicate the fractional numbers as String types, not JavaScript Number type.
+		 *
+		 * @param {Number|String} ch1 Color channel value
+		 * @param {Number|String} ch2 Color channel value
+		 * @param {Number|String} ch3 Color channel value
+		 * @param {Number|String} ch4 Color channel value
+		 *
+		 * @function
+		 * @returns {jsPDF}
+		 * @methodOf jsPDF#
+		 * @name setFillColor
+		 */
+		API.setFillColor = function(ch1, ch2, ch3, ch4) {
+			var color;
+
+			if (ch2 === undefined || (ch4 === undefined && ch1 === ch2 === ch3)) {
+				// Gray color space.
+				if (typeof ch1 === 'string') {
+					color = ch1 + ' g';
+				} else {
+					color = f2(ch1 / 255) + ' g';
+				}
+			} else if (ch4 === undefined) {
+				// RGB
+				if (typeof ch1 === 'string') {
+					color = [ch1, ch2, ch3, 'rg'].join(' ');
+				} else {
+					color = [f2(ch1 / 255), f2(ch2 / 255), f2(ch3 / 255), 'rg'].join(' ');
+				}
+			} else {
+				// CMYK
+				if (typeof ch1 === 'string') {
+					color = [ch1, ch2, ch3, ch4, 'k'].join(' ');
+				} else {
+					color = [f2(ch1), f2(ch2), f2(ch3), f2(ch4), 'k'].join(' ');
+				}
+			}
+
+			out(color);
+			return this;
+		};
+
+		/**
+		 * Sets the text color for upcoming elements.
+		 * If only one, first argument is given,
+		 * treats the value as gray-scale color value.
+		 *
+		 * @param {Number} r Red channel color value in range 0-255 or {String} r color value in hexadecimal, example: '#FFFFFF'
+		 * @param {Number} g Green channel color value in range 0-255
+		 * @param {Number} b Blue channel color value in range 0-255
+		 * @function
+		 * @returns {jsPDF}
+		 * @methodOf jsPDF#
+		 * @name setTextColor
+		 */
+		API.setTextColor = function(r, g, b) {
+			if ((typeof r === 'string') && /^#[0-9A-Fa-f]{6}$/.test(r)) {
+				var hex = parseInt(r.substr(1), 16);
+				r = (hex >> 16) & 255;
+				g = (hex >> 8) & 255;
+				b = (hex & 255);
+			}
+
+			if ((r === 0 && g === 0 && b === 0) || (typeof g === 'undefined')) {
+				textColor = f3(r / 255) + ' g';
+			} else {
+				textColor = [f3(r / 255), f3(g / 255), f3(b / 255), 'rg'].join(' ');
+			}
+			return this;
+		};
+
+		/**
+		 * Is an Object providing a mapping from human-readable to
+		 * integer flag values designating the varieties of line cap
+		 * and join styles.
+		 *
+		 * @returns {Object}
+		 * @fieldOf jsPDF#
+		 * @name CapJoinStyles
+		 */
+		API.CapJoinStyles = {
+			0 : 0,
+			'butt' : 0,
+			'but' : 0,
+			'miter' : 0,
+			1 : 1,
+			'round' : 1,
+			'rounded' : 1,
+			'circle' : 1,
+			2 : 2,
+			'projecting' : 2,
+			'project' : 2,
+			'square' : 2,
+			'bevel' : 2
+		};
+
+		/**
+		 * Sets the line cap styles
+		 * See {jsPDF.CapJoinStyles} for variants
+		 *
+		 * @param {String|Number} style A string or number identifying the type of line cap
+		 * @function
+		 * @returns {jsPDF}
+		 * @methodOf jsPDF#
+		 * @name setLineCap
+		 */
+		API.setLineCap = function(style) {
+			var id = this.CapJoinStyles[style];
+			if (id === undefined) {
+				throw new Error("Line cap style of '" + style + "' is not recognized. See or extend .CapJoinStyles property for valid styles");
+			}
+			lineCapID = id;
+			out(id + ' J');
+
+			return this;
+		};
+
+		/**
+		 * Sets the line join styles
+		 * See {jsPDF.CapJoinStyles} for variants
+		 *
+		 * @param {String|Number} style A string or number identifying the type of line join
+		 * @function
+		 * @returns {jsPDF}
+		 * @methodOf jsPDF#
+		 * @name setLineJoin
+		 */
+		API.setLineJoin = function(style) {
+			var id = this.CapJoinStyles[style];
+			if (id === undefined) {
+				throw new Error("Line join style of '" + style + "' is not recognized. See or extend .CapJoinStyles property for valid styles");
+			}
+			lineJoinID = id;
+			out(id + ' j');
+
+			return this;
+		};
+
+		// Output is both an internal (for plugins) and external function
+		API.output = output;
+
+		/**
+		 * Saves as PDF document. An alias of jsPDF.output('save', 'filename.pdf')
+		 * @param  {String} filename The filename including extension.
+		 *
+		 * @function
+		 * @returns {jsPDF}
+		 * @methodOf jsPDF#
+		 * @name save
+		 */
+		API.save = function(filename) {
+			API.output('save', filename);
+		};
+
+		// applying plugins (more methods) ON TOP of built-in API.
+		// this is intentional as we allow plugins to override
+		// built-ins
+		for (var plugin in jsPDF.API) {
+			if (jsPDF.API.hasOwnProperty(plugin)) {
+				if (plugin === 'events' && jsPDF.API.events.length) {
+					(function(events, newEvents) {
+
+						// jsPDF.API.events is a JS Array of Arrays
+						// where each Array is a pair of event name, handler
+						// Events were added by plugins to the jsPDF instantiator.
+						// These are always added to the new instance and some ran
+						// during instantiation.
+						var eventname,handler_and_args,i;
+
+						for (i = newEvents.length - 1; i !== -1; i--) {
+							// subscribe takes 3 args: 'topic', function, runonce_flag
+							// if undefined, runonce is false.
+							// users can attach callback directly,
+							// or they can attach an array with [callback, runonce_flag]
+							// that's what the "apply" magic is for below.
+							eventname = newEvents[i][0];
+							handler_and_args = newEvents[i][1];
+							events.subscribe.apply(
+								events,
+								[eventname].concat(
+									typeof handler_and_args === 'function' ?
+										[handler_and_args] : handler_and_args));
+						}
+					}(events, jsPDF.API.events));
+				} else {
+					API[plugin] = jsPDF.API[plugin];
+				}
+			}
+		}
+
+		//////////////////////////////////////////////////////
+		// continuing initialization of jsPDF Document object
+		//////////////////////////////////////////////////////
+		// Add the first page automatically
+		addFonts();
+		activeFontKey = 'F1';
+		_addPage(format, orientation);
+
+		events.publish('initialized');
+		return API;
+	}
+
+	/**
+	 * jsPDF.API is a STATIC property of jsPDF class.
+	 * jsPDF.API is an object you can add methods and properties to.
+	 * The methods / properties you add will show up in new jsPDF objects.
+	 *
+	 * One property is prepopulated. It is the 'events' Object. Plugin authors can add topics,
+	 * callbacks to this object. These will be reassigned to all new instances of jsPDF.
+	 * Examples:
+	 * jsPDF.API.events['initialized'] = function(){ 'this' is API object }
+	 * jsPDF.API.events['addFont'] = function(added_font_object){ 'this' is API object }
+	 *
+	 * @static
+	 * @public
+	 * @memberOf jsPDF
+	 * @name API
+	 *
+	 * @example
+	 * jsPDF.API.mymethod = function(){
+	 *   // 'this' will be ref to internal API object. see jsPDF source
+	 *   // , so you can refer to built-in methods like so:
+	 *   //     this.line(....)
+	 *   //     this.text(....)
+	 * }
+	 * var pdfdoc = new jsPDF()
+	 * pdfdoc.mymethod() // <- !!!!!!
+	 */
+	jsPDF.API = {events:[]};
+	jsPDF.version = "1.0.272-debug 2014-09-29T15:09:diegocr";
+
+	if (typeof define === 'function' && define.amd) {
+		define('jsPDF', function() {
+			return jsPDF;
+		});
+	} else {
+		global.jsPDF = jsPDF;
+	}
+	return jsPDF;
+}(typeof self !== "undefined" && self || typeof window !== "undefined" && window || this));
+/**
+ * jsPDF addHTML PlugIn
+ * Copyright (c) 2014 Diego Casorran
+ *
+ * Licensed under the MIT License.
+ * http://opensource.org/licenses/mit-license
+ */
+
+(function (jsPDFAPI) {
+	'use strict';
+
+	/**
+	 * Renders an HTML element to canvas object which added as an image to the PDF
+	 *
+	 * This PlugIn requires html2canvas: https://github.com/niklasvh/html2canvas
+	 *            OR rasterizeHTML: https://github.com/cburgmer/rasterizeHTML.js
+	 *
+	 * @public
+	 * @function
+	 * @param element {Mixed} HTML Element, or anything supported by html2canvas.
+	 * @param x {Number} starting X coordinate in jsPDF instance's declared units.
+	 * @param y {Number} starting Y coordinate in jsPDF instance's declared units.
+	 * @param options {Object} Additional options, check the code below.
+	 * @param callback {Function} to call when the rendering has finished.
+	 *
+	 * NOTE: Every parameter is optional except 'element' and 'callback', in such
+	 *       case the image is positioned at 0x0 covering the whole PDF document
+	 *       size. Ie, to easily take screenshoots of webpages saving them to PDF.
+	 */
+	jsPDFAPI.addHTML = function (element, x, y, options, callback) {
+		'use strict';
+
+		if(typeof html2canvas === 'undefined' && typeof rasterizeHTML === 'undefined')
+			throw new Error('You need either '
+				+'https://github.com/niklasvh/html2canvas'
+				+' or https://github.com/cburgmer/rasterizeHTML.js');
+
+		if(typeof x !== 'number') {
+			options = x;
+			callback = y;
+		}
+
+		if(typeof options === 'function') {
+			callback = options;
+			options = null;
+		}
+
+		var I = this.internal, K = I.scaleFactor, W = I.pageSize.width, H = I.pageSize.height;
+
+		options = options || {};
+		options.onrendered = function(obj) {
+			x = parseInt(x) || 0;
+			y = parseInt(y) || 0;
+			var dim = options.dim || {};
+			var h = dim.h || 0;
+			var w = dim.w || Math.min(W,obj.width/K) - x;
+
+			var format = 'JPEG';
+			if(options.format)
+				format = options.format;
+
+			if(obj.height > H && options.pagesplit) {
+				var crop = function() {
+					var cy = 0;
+					while(1) {
+						var canvas = document.createElement('canvas');
+						canvas.width = Math.min(W*K,obj.width);
+						canvas.height = Math.min(H*K,obj.height-cy);
+						var ctx = canvas.getContext('2d');
+						ctx.drawImage(obj,0,cy,obj.width,canvas.height,0,0,canvas.width,canvas.height);
+						var args = [canvas, x,cy?0:y,canvas.width/K,canvas.height/K, format,null,'SLOW'];
+						this.addImage.apply(this, args);
+						cy += canvas.height;
+						if(cy >= obj.height) break;
+						this.addPage();
+					}
+					callback(w,cy,null,args);
+				}.bind(this);
+				if(obj.nodeName === 'CANVAS') {
+					var img = new Image();
+					img.onload = crop;
+					img.src = obj.toDataURL("image/png");
+					obj = img;
+				} else {
+					crop();
+				}
+			} else {
+				var alias = Math.random().toString(35);
+				var args = [obj, x,y,w,h, format,alias,'SLOW'];
+
+				this.addImage.apply(this, args);
+
+				callback(w,h,alias,args);
+			}
+		}.bind(this);
+
+		if(typeof html2canvas !== 'undefined' && !options.rstz) {
+			return html2canvas(element, options);
+		}
+
+		if(typeof rasterizeHTML !== 'undefined') {
+			var meth = 'drawDocument';
+			if(typeof element === 'string') {
+				meth = /^http/.test(element) ? 'drawURL' : 'drawHTML';
+			}
+			options.width = options.width || (W*K);
+			return rasterizeHTML[meth](element, void 0, options).then(function(r) {
+				options.onrendered(r.image);
+			}, function(e) {
+				callback(null,e);
+			});
+		}
+
+		return null;
+	};
+})(jsPDF.API);
+/** @preserve
+ * jsPDF addImage plugin
+ * Copyright (c) 2012 Jason Siefken, https://github.com/siefkenj/
+ *               2013 Chris Dowling, https://github.com/gingerchris
+ *               2013 Trinh Ho, https://github.com/ineedfat
+ *               2013 Edwin Alejandro Perez, https://github.com/eaparango
+ *               2013 Norah Smith, https://github.com/burnburnrocket
+ *               2014 Diego Casorran, https://github.com/diegocr
+ *               2014 James Robb, https://github.com/jamesbrobb
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+;(function(jsPDFAPI) {
+	'use strict'
+
+	var namespace = 'addImage_',
+		supported_image_types = ['jpeg', 'jpg', 'png'];
+
+	// Image functionality ported from pdf.js
+	var putImage = function(img) {
+
+		var objectNumber = this.internal.newObject()
+		, out = this.internal.write
+		, putStream = this.internal.putStream
+
+		img['n'] = objectNumber
+
+		out('<</Type /XObject')
+		out('/Subtype /Image')
+		out('/Width ' + img['w'])
+		out('/Height ' + img['h'])
+		if (img['cs'] === this.color_spaces.INDEXED) {
+			out('/ColorSpace [/Indexed /DeviceRGB '
+					// if an indexed png defines more than one colour with transparency, we've created a smask
+					+ (img['pal'].length / 3 - 1) + ' ' + ('smask' in img ? objectNumber + 2 : objectNumber + 1)
+					+ ' 0 R]');
+		} else {
+			out('/ColorSpace /' + img['cs']);
+			if (img['cs'] === this.color_spaces.DEVICE_CMYK) {
+				out('/Decode [1 0 1 0 1 0 1 0]');
+			}
+		}
+		out('/BitsPerComponent ' + img['bpc']);
+		if ('f' in img) {
+			out('/Filter /' + img['f']);
+		}
+		if ('dp' in img) {
+			out('/DecodeParms <<' + img['dp'] + '>>');
+		}
+		if ('trns' in img && img['trns'].constructor == Array) {
+			var trns = '',
+				i = 0,
+				len = img['trns'].length;
+			for (; i < len; i++)
+				trns += (img['trns'][i] + ' ' + img['trns'][i] + ' ');
+			out('/Mask [' + trns + ']');
+		}
+		if ('smask' in img) {
+			out('/SMask ' + (objectNumber + 1) + ' 0 R');
+		}
+		out('/Length ' + img['data'].length + '>>');
+
+		putStream(img['data']);
+
+		out('endobj');
+
+		// Soft mask
+		if ('smask' in img) {
+			var dp = '/Predictor 15 /Colors 1 /BitsPerComponent ' + img['bpc'] + ' /Columns ' + img['w'];
+			var smask = {'w': img['w'], 'h': img['h'], 'cs': 'DeviceGray', 'bpc': img['bpc'], 'dp': dp, 'data': img['smask']};
+			if ('f' in img)
+				smask.f = img['f'];
+			putImage.call(this, smask);
+		}
+
+	    //Palette
+		if (img['cs'] === this.color_spaces.INDEXED) {
+
+			this.internal.newObject();
+			//out('<< /Filter / ' + img['f'] +' /Length ' + img['pal'].length + '>>');
+			//putStream(zlib.compress(img['pal']));
+			out('<< /Length ' + img['pal'].length + '>>');
+			putStream(this.arrayBufferToBinaryString(new Uint8Array(img['pal'])));
+			out('endobj');
+		}
+	}
+	, putResourcesCallback = function() {
+		var images = this.internal.collections[namespace + 'images']
+		for ( var i in images ) {
+			putImage.call(this, images[i])
+		}
+	}
+	, putXObjectsDictCallback = function(){
+		var images = this.internal.collections[namespace + 'images']
+		, out = this.internal.write
+		, image
+		for (var i in images) {
+			image = images[i]
+			out(
+				'/I' + image['i']
+				, image['n']
+				, '0'
+				, 'R'
+			)
+		}
+	}
+	, checkCompressValue = function(value) {
+		if(value && typeof value === 'string')
+			value = value.toUpperCase();
+		return value in jsPDFAPI.image_compression ? value : jsPDFAPI.image_compression.NONE;
+	}
+	, getImages = function() {
+		var images = this.internal.collections[namespace + 'images'];
+		//first run, so initialise stuff
+		if(!images) {
+			this.internal.collections[namespace + 'images'] = images = {};
+			this.internal.events.subscribe('putResources', putResourcesCallback);
+			this.internal.events.subscribe('putXobjectDict', putXObjectsDictCallback);
+		}
+
+		return images;
+	}
+	, getImageIndex = function(images) {
+		var imageIndex = 0;
+
+		if (images){
+			// this is NOT the first time this method is ran on this instance of jsPDF object.
+			imageIndex = Object.keys ?
+			Object.keys(images).length :
+			(function(o){
+				var i = 0
+				for (var e in o){if(o.hasOwnProperty(e)){ i++ }}
+				return i
+			})(images)
+		}
+
+		return imageIndex;
+	}
+	, notDefined = function(value) {
+		return typeof value === 'undefined' || value === null;
+	}
+	, generateAliasFromData = function(data) {
+		return typeof data === 'string' && jsPDFAPI.sHashCode(data);
+	}
+	, doesNotSupportImageType = function(type) {
+		return supported_image_types.indexOf(type) === -1;
+	}
+	, processMethodNotEnabled = function(type) {
+		return typeof jsPDFAPI['process' + type.toUpperCase()] !== 'function';
+	}
+	, isDOMElement = function(object) {
+		return typeof object === 'object' && object.nodeType === 1;
+	}
+	, createDataURIFromElement = function(element, format, angle) {
+
+		//if element is an image which uses data url defintion, just return the dataurl
+		if (element.nodeName === 'IMG' && element.hasAttribute('src')) {
+			var src = ''+element.getAttribute('src');
+			if (!angle && src.indexOf('data:image/') === 0) return src;
+
+			// only if the user doesn't care about a format
+			if (!format && /\.png(?:[?#].*)?$/i.test(src)) format = 'png';
+		}
+
+		if(element.nodeName === 'CANVAS') {
+			var canvas = element;
+		} else {
+			var canvas = document.createElement('canvas');
+			canvas.width = element.clientWidth || element.width;
+			canvas.height = element.clientHeight || element.height;
+
+			var ctx = canvas.getContext('2d');
+			if (!ctx) {
+				throw ('addImage requires canvas to be supported by browser.');
+			}
+			if (angle) {
+				var x, y, b, c, s, w, h, to_radians = Math.PI/180, angleInRadians;
+
+				if (typeof angle === 'object') {
+					x = angle.x;
+					y = angle.y;
+					b = angle.bg;
+					angle = angle.angle;
+				}
+				angleInRadians = angle*to_radians;
+				c = Math.abs(Math.cos(angleInRadians));
+				s = Math.abs(Math.sin(angleInRadians));
+				w = canvas.width;
+				h = canvas.height;
+				canvas.width = h * s + w * c;
+				canvas.height = h * c + w * s;
+
+				if (isNaN(x)) x = canvas.width / 2;
+				if (isNaN(y)) y = canvas.height / 2;
+
+				ctx.clearRect(0,0,canvas.width, canvas.height);
+				ctx.fillStyle = b || 'white';
+				ctx.fillRect(0, 0, canvas.width, canvas.height);
+				ctx.save();
+				ctx.translate(x, y);
+				ctx.rotate(angleInRadians);
+				ctx.drawImage(element, -(w/2), -(h/2));
+				ctx.rotate(-angleInRadians);
+				ctx.translate(-x, -y);
+				ctx.restore();
+			} else {
+				ctx.drawImage(element, 0, 0, canvas.width, canvas.height);
+			}
+		}
+		return canvas.toDataURL((''+format).toLowerCase() == 'png' ? 'image/png' : 'image/jpeg');
+	}
+	,checkImagesForAlias = function(alias, images) {
+		var cached_info;
+		if(images) {
+			for(var e in images) {
+				if(alias === images[e].alias) {
+					cached_info = images[e];
+					break;
+				}
+			}
+		}
+		return cached_info;
+	}
+	,determineWidthAndHeight = function(w, h, info) {
+		if (!w && !h) {
+			w = -96;
+			h = -96;
+		}
+		if (w < 0) {
+			w = (-1) * info['w'] * 72 / w / this.internal.scaleFactor;
+		}
+		if (h < 0) {
+			h = (-1) * info['h'] * 72 / h / this.internal.scaleFactor;
+		}
+		if (w === 0) {
+			w = h * info['w'] / info['h'];
+		}
+		if (h === 0) {
+			h = w * info['h'] / info['w'];
+		}
+
+		return [w, h];
+	}
+	, writeImageToPDF = function(x, y, w, h, info, index, images) {
+		var dims = determineWidthAndHeight.call(this, w, h, info),
+			coord = this.internal.getCoordinateString,
+			vcoord = this.internal.getVerticalCoordinateString;
+
+		w = dims[0];
+		h = dims[1];
+
+		images[index] = info;
+
+		this.internal.write(
+			'q'
+			, coord(w)
+			, '0 0'
+			, coord(h) // TODO: check if this should be shifted by vcoord
+			, coord(x)
+			, vcoord(y + h)
+			, 'cm /I'+info['i']
+			, 'Do Q'
+		)
+	};
+
+	/**
+	 * COLOR SPACES
+	 */
+	jsPDFAPI.color_spaces = {
+		DEVICE_RGB:'DeviceRGB',
+		DEVICE_GRAY:'DeviceGray',
+		DEVICE_CMYK:'DeviceCMYK',
+		CAL_GREY:'CalGray',
+		CAL_RGB:'CalRGB',
+		LAB:'Lab',
+		ICC_BASED:'ICCBased',
+		INDEXED:'Indexed',
+		PATTERN:'Pattern',
+		SEPERATION:'Seperation',
+		DEVICE_N:'DeviceN'
+	};
+
+	/**
+	 * DECODE METHODS
+	 */
+	jsPDFAPI.decode = {
+		DCT_DECODE:'DCTDecode',
+		FLATE_DECODE:'FlateDecode',
+		LZW_DECODE:'LZWDecode',
+		JPX_DECODE:'JPXDecode',
+		JBIG2_DECODE:'JBIG2Decode',
+		ASCII85_DECODE:'ASCII85Decode',
+		ASCII_HEX_DECODE:'ASCIIHexDecode',
+		RUN_LENGTH_DECODE:'RunLengthDecode',
+		CCITT_FAX_DECODE:'CCITTFaxDecode'
+	};
+
+	/**
+	 * IMAGE COMPRESSION TYPES
+	 */
+	jsPDFAPI.image_compression = {
+		NONE: 'NONE',
+		FAST: 'FAST',
+		MEDIUM: 'MEDIUM',
+		SLOW: 'SLOW'
+	};
+
+	jsPDFAPI.sHashCode = function(str) {
+		return Array.prototype.reduce && str.split("").reduce(function(a,b){a=((a<<5)-a)+b.charCodeAt(0);return a&a},0);
+	};
+
+	jsPDFAPI.isString = function(object) {
+		return typeof object === 'string';
+	};
+
+	/**
+	 * Strips out and returns info from a valid base64 data URI
+	 * @param {String[dataURI]} a valid data URI of format 'data:[<MIME-type>][;base64],<data>'
+	 * @returns an Array containing the following
+	 * [0] the complete data URI
+	 * [1] <MIME-type>
+	 * [2] format - the second part of the mime-type i.e 'png' in 'image/png'
+	 * [4] <data>
+	 */
+	jsPDFAPI.extractInfoFromBase64DataURI = function(dataURI) {
+		return /^data:([\w]+?\/([\w]+?));base64,(.+?)$/g.exec(dataURI);
+	};
+
+	/**
+	 * Check to see if ArrayBuffer is supported
+	 */
+	jsPDFAPI.supportsArrayBuffer = function() {
+		return typeof ArrayBuffer !== 'undefined' && typeof Uint8Array !== 'undefined';
+	};
+
+	/**
+	 * Tests supplied object to determine if ArrayBuffer
+	 * @param {Object[object]}
+	 */
+	jsPDFAPI.isArrayBuffer = function(object) {
+		if(!this.supportsArrayBuffer())
+	        return false;
+		return object instanceof ArrayBuffer;
+	};
+
+	/**
+	 * Tests supplied object to determine if it implements the ArrayBufferView (TypedArray) interface
+	 * @param {Object[object]}
+	 */
+	jsPDFAPI.isArrayBufferView = function(object) {
+		if(!this.supportsArrayBuffer())
+	        return false;
+		if(typeof Uint32Array === 'undefined')
+			return false;
+		return (object instanceof Int8Array ||
+				object instanceof Uint8Array ||
+				(typeof Uint8ClampedArray !== 'undefined' && object instanceof Uint8ClampedArray) ||
+				object instanceof Int16Array ||
+				object instanceof Uint16Array ||
+				object instanceof Int32Array ||
+				object instanceof Uint32Array ||
+				object instanceof Float32Array ||
+				object instanceof Float64Array );
+	};
+
+	/**
+	 * Exactly what it says on the tin
+	 */
+	jsPDFAPI.binaryStringToUint8Array = function(binary_string) {
+		/*
+		 * not sure how efficient this will be will bigger files. Is there a native method?
+		 */
+		var len = binary_string.length;
+	    var bytes = new Uint8Array( len );
+	    for (var i = 0; i < len; i++) {
+	        bytes[i] = binary_string.charCodeAt(i);
+	    }
+	    return bytes;
+	};
+
+	/**
+	 * @see this discussion
+	 * http://stackoverflow.com/questions/6965107/converting-between-strings-and-arraybuffers
+	 *
+	 * As stated, i imagine the method below is highly inefficent for large files.
+	 *
+	 * Also of note from Mozilla,
+	 *
+	 * "However, this is slow and error-prone, due to the need for multiple conversions (especially if the binary data is not actually byte-format data, but, for example, 32-bit integers or floats)."
+	 *
+	 * https://developer.mozilla.org/en-US/Add-ons/Code_snippets/StringView
+	 *
+	 * Although i'm strugglig to see how StringView solves this issue? Doesn't appear to be a direct method for conversion?
+	 *
+	 * Async method using Blob and FileReader could be best, but i'm not sure how to fit it into the flow?
+	 */
+	jsPDFAPI.arrayBufferToBinaryString = function(buffer) {
+		if(this.isArrayBuffer(buffer))
+			buffer = new Uint8Array(buffer);
+
+	    var binary_string = '';
+	    var len = buffer.byteLength;
+	    for (var i = 0; i < len; i++) {
+	        binary_string += String.fromCharCode(buffer[i]);
+	    }
+	    return binary_string;
+	    /*
+	     * Another solution is the method below - convert array buffer straight to base64 and then use atob
+	     */
+		//return atob(this.arrayBufferToBase64(buffer));
+	};
+
+	/**
+	 * Converts an ArrayBuffer directly to base64
+	 *
+	 * Taken from here
+	 *
+	 * http://jsperf.com/encoding-xhr-image-data/31
+	 *
+	 * Need to test if this is a better solution for larger files
+	 *
+	 */
+	jsPDFAPI.arrayBufferToBase64 = function(arrayBuffer) {
+		var base64    = ''
+		var encodings = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
+
+		var bytes         = new Uint8Array(arrayBuffer)
+		var byteLength    = bytes.byteLength
+		var byteRemainder = byteLength % 3
+		var mainLength    = byteLength - byteRemainder
+
+		var a, b, c, d
+		var chunk
+
+		// Main loop deals with bytes in chunks of 3
+		for (var i = 0; i < mainLength; i = i + 3) {
+			// Combine the three bytes into a single integer
+			chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2]
+
+			// Use bitmasks to extract 6-bit segments from the triplet
+			a = (chunk & 16515072) >> 18 // 16515072 = (2^6 - 1) << 18
+			b = (chunk & 258048)   >> 12 // 258048   = (2^6 - 1) << 12
+			c = (chunk & 4032)     >>  6 // 4032     = (2^6 - 1) << 6
+			d = chunk & 63               // 63       = 2^6 - 1
+
+			// Convert the raw binary segments to the appropriate ASCII encoding
+			base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d]
+		}
+
+		// Deal with the remaining bytes and padding
+		if (byteRemainder == 1) {
+			chunk = bytes[mainLength]
+
+			a = (chunk & 252) >> 2 // 252 = (2^6 - 1) << 2
+
+			// Set the 4 least significant bits to zero
+			b = (chunk & 3)   << 4 // 3   = 2^2 - 1
+
+			base64 += encodings[a] + encodings[b] + '=='
+		} else if (byteRemainder == 2) {
+			chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1]
+
+			a = (chunk & 64512) >> 10 // 64512 = (2^6 - 1) << 10
+			b = (chunk & 1008)  >>  4 // 1008  = (2^6 - 1) << 4
+
+			// Set the 2 least significant bits to zero
+			c = (chunk & 15)    <<  2 // 15    = 2^4 - 1
+
+			base64 += encodings[a] + encodings[b] + encodings[c] + '='
+		}
+
+		return base64
+	};
+
+	jsPDFAPI.createImageInfo = function(data, wd, ht, cs, bpc, f, imageIndex, alias, dp, trns, pal, smask) {
+		var info = {
+				alias:alias,
+				w : wd,
+				h : ht,
+				cs : cs,
+				bpc : bpc,
+				i : imageIndex,
+				data : data
+				// n: objectNumber will be added by putImage code
+			};
+
+		if(f) info.f = f;
+		if(dp) info.dp = dp;
+		if(trns) info.trns = trns;
+		if(pal) info.pal = pal;
+		if(smask) info.smask = smask;
+
+		return info;
+	};
+
+	jsPDFAPI.addImage = function(imageData, format, x, y, w, h, alias, compression, rotation) {
+		'use strict'
+
+		if(typeof format !== 'string') {
+			var tmp = h;
+			h = w;
+			w = y;
+			y = x;
+			x = format;
+			format = tmp;
+		}
+
+		if (typeof imageData === 'object' && !isDOMElement(imageData) && "imageData" in imageData) {
+			var options = imageData;
+
+			imageData = options.imageData;
+			format = options.format || format;
+			x = options.x || x || 0;
+			y = options.y || y || 0;
+			w = options.w || w;
+			h = options.h || h;
+			alias = options.alias || alias;
+			compression = options.compression || compression;
+			rotation = options.rotation || options.angle || rotation;
+		}
+
+		if (isNaN(x) || isNaN(y))
+		{
+			console.error('jsPDF.addImage: Invalid coordinates', arguments);
+			throw new Error('Invalid coordinates passed to jsPDF.addImage');
+		}
+
+		var images = getImages.call(this), info;
+
+		if (!(info = checkImagesForAlias(imageData, images))) {
+			var dataAsBinaryString;
+
+			if(isDOMElement(imageData))
+				imageData = createDataURIFromElement(imageData, format, rotation);
+
+			if(notDefined(alias))
+				alias = generateAliasFromData(imageData);
+
+			if (!(info = checkImagesForAlias(alias, images))) {
+
+				if(this.isString(imageData)) {
+
+					var base64Info = this.extractInfoFromBase64DataURI(imageData);
+
+					if(base64Info) {
+
+						format = base64Info[2];
+						imageData = atob(base64Info[3]);//convert to binary string
+
+					} else {
+
+						if (imageData.charCodeAt(0) === 0x89 &&
+							imageData.charCodeAt(1) === 0x50 &&
+							imageData.charCodeAt(2) === 0x4e &&
+							imageData.charCodeAt(3) === 0x47  )  format = 'png';
+					}
+				}
+				format = (format || 'JPEG').toLowerCase();
+
+				if(doesNotSupportImageType(format))
+					throw new Error('addImage currently only supports formats ' + supported_image_types + ', not \''+format+'\'');
+
+				if(processMethodNotEnabled(format))
+					throw new Error('please ensure that the plugin for \''+format+'\' support is added');
+
+				/**
+				 * need to test if it's more efficent to convert all binary strings
+				 * to TypedArray - or should we just leave and process as string?
+				 */
+				if(this.supportsArrayBuffer()) {
+					dataAsBinaryString = imageData;
+					imageData = this.binaryStringToUint8Array(imageData);
+				}
+
+				info = this['process' + format.toUpperCase()](
+					imageData,
+					getImageIndex(images),
+					alias,
+					checkCompressValue(compression),
+					dataAsBinaryString
+				);
+
+				if(!info)
+					throw new Error('An unkwown error occurred whilst processing the image');
+			}
+		}
+
+		writeImageToPDF.call(this, x, y, w, h, info, info.i, images);
+
+		return this
+	};
+
+	/**
+	 * JPEG SUPPORT
+	 **/
+
+	//takes a string imgData containing the raw bytes of
+	//a jpeg image and returns [width, height]
+	//Algorithm from: http://www.64lines.com/jpeg-width-height
+	var getJpegSize = function(imgData) {
+		'use strict'
+		var width, height, numcomponents;
+		// Verify we have a valid jpeg header 0xff,0xd8,0xff,0xe0,?,?,'J','F','I','F',0x00
+		if (!imgData.charCodeAt(0) === 0xff ||
+			!imgData.charCodeAt(1) === 0xd8 ||
+			!imgData.charCodeAt(2) === 0xff ||
+			!imgData.charCodeAt(3) === 0xe0 ||
+			!imgData.charCodeAt(6) === 'J'.charCodeAt(0) ||
+			!imgData.charCodeAt(7) === 'F'.charCodeAt(0) ||
+			!imgData.charCodeAt(8) === 'I'.charCodeAt(0) ||
+			!imgData.charCodeAt(9) === 'F'.charCodeAt(0) ||
+			!imgData.charCodeAt(10) === 0x00) {
+				throw new Error('getJpegSize requires a binary string jpeg file')
+		}
+		var blockLength = imgData.charCodeAt(4)*256 + imgData.charCodeAt(5);
+		var i = 4, len = imgData.length;
+		while ( i < len ) {
+			i += blockLength;
+			if (imgData.charCodeAt(i) !== 0xff) {
+				throw new Error('getJpegSize could not find the size of the image');
+			}
+			if (imgData.charCodeAt(i+1) === 0xc0 || //(SOF) Huffman  - Baseline DCT
+			    imgData.charCodeAt(i+1) === 0xc1 || //(SOF) Huffman  - Extended sequential DCT
+			    imgData.charCodeAt(i+1) === 0xc2 || // Progressive DCT (SOF2)
+			    imgData.charCodeAt(i+1) === 0xc3 || // Spatial (sequential) lossless (SOF3)
+			    imgData.charCodeAt(i+1) === 0xc4 || // Differential sequential DCT (SOF5)
+			    imgData.charCodeAt(i+1) === 0xc5 || // Differential progressive DCT (SOF6)
+			    imgData.charCodeAt(i+1) === 0xc6 || // Differential spatial (SOF7)
+			    imgData.charCodeAt(i+1) === 0xc7) {
+				height = imgData.charCodeAt(i+5)*256 + imgData.charCodeAt(i+6);
+				width = imgData.charCodeAt(i+7)*256 + imgData.charCodeAt(i+8);
+                numcomponents = imgData.charCodeAt(i+9);
+				return [width, height, numcomponents];
+			} else {
+				i += 2;
+				blockLength = imgData.charCodeAt(i)*256 + imgData.charCodeAt(i+1)
+			}
+		}
+	}
+	, getJpegSizeFromBytes = function(data) {
+
+		var hdr = (data[0] << 8) | data[1];
+
+		if(hdr !== 0xFFD8)
+			throw new Error('Supplied data is not a JPEG');
+
+		var len = data.length,
+			block = (data[4] << 8) + data[5],
+			pos = 4,
+			bytes, width, height, numcomponents;
+
+		while(pos < len) {
+			pos += block;
+			bytes = readBytes(data, pos);
+			block = (bytes[2] << 8) + bytes[3];
+			if((bytes[1] === 0xC0 || bytes[1] === 0xC2) && bytes[0] === 0xFF && block > 7) {
+				bytes = readBytes(data, pos + 5);
+				width = (bytes[2] << 8) + bytes[3];
+				height = (bytes[0] << 8) + bytes[1];
+                numcomponents = bytes[4];
+				return {width:width, height:height, numcomponents: numcomponents};
+			}
+
+			pos+=2;
+		}
+
+		throw new Error('getJpegSizeFromBytes could not find the size of the image');
+	}
+	, readBytes = function(data, offset) {
+		return data.subarray(offset, offset+ 5);
+	};
+
+	jsPDFAPI.processJPEG = function(data, index, alias, compression, dataAsBinaryString) {
+		'use strict'
+		var colorSpace = this.color_spaces.DEVICE_RGB,
+			filter = this.decode.DCT_DECODE,
+			bpc = 8,
+			dims;
+
+		if(this.isString(data)) {
+			dims = getJpegSize(data);
+			return this.createImageInfo(data, dims[0], dims[1], dims[3] == 1 ? this.color_spaces.DEVICE_GRAY:colorSpace, bpc, filter, index, alias);
+		}
+
+		if(this.isArrayBuffer(data))
+			data = new Uint8Array(data);
+
+		if(this.isArrayBufferView(data)) {
+
+			dims = getJpegSizeFromBytes(data);
+
+			// if we already have a stored binary string rep use that
+			data = dataAsBinaryString || this.arrayBufferToBinaryString(data);
+
+			return this.createImageInfo(data, dims.width, dims.height, dims.numcomponents == 1 ? this.color_spaces.DEVICE_GRAY:colorSpace, bpc, filter, index, alias);
+		}
+
+		return null;
+	};
+
+	jsPDFAPI.processJPG = function(/*data, index, alias, compression, dataAsBinaryString*/) {
+		return this.processJPEG.apply(this, arguments);
+	}
+
+})(jsPDF.API);
+(function (jsPDFAPI) {
+	'use strict';
+
+	jsPDFAPI.autoPrint = function () {
+		'use strict'
+		var refAutoPrintTag;
+
+		this.internal.events.subscribe('postPutResources', function () {
+			refAutoPrintTag = this.internal.newObject()
+				this.internal.write("<< /S/Named /Type/Action /N/Print >>", "endobj");
+		});
+
+		this.internal.events.subscribe("putCatalog", function () {
+			this.internal.write("/OpenAction " + refAutoPrintTag + " 0" + " R");
+		});
+		return this;
+	};
+})(jsPDF.API);
+/** ====================================================================
+ * jsPDF Cell plugin
+ * Copyright (c) 2013 Youssef Beddad, youssef.beddad@gmail.com
+ *               2013 Eduardo Menezes de Morais, eduardo.morais@usp.br
+ *               2013 Lee Driscoll, https://github.com/lsdriscoll
+ *               2014 Juan Pablo Gaviria, https://github.com/juanpgaviria
+ *               2014 James Hall, james@parall.ax
+ *               2014 Diego Casorran, https://github.com/diegocr
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ * ====================================================================
+ */
+
+(function (jsPDFAPI) {
+    'use strict';
+    /*jslint browser:true */
+    /*global document: false, jsPDF */
+
+    var fontName,
+        fontSize,
+        fontStyle,
+        padding = 3,
+        margin = 13,
+        headerFunction,
+        lastCellPos = { x: undefined, y: undefined, w: undefined, h: undefined, ln: undefined },
+        pages = 1,
+        setLastCellPosition = function (x, y, w, h, ln) {
+            lastCellPos = { 'x': x, 'y': y, 'w': w, 'h': h, 'ln': ln };
+        },
+        getLastCellPosition = function () {
+            return lastCellPos;
+        },
+        NO_MARGINS = {left:0, top:0, bottom: 0};
+
+    jsPDFAPI.setHeaderFunction = function (func) {
+        headerFunction = func;
+    };
+
+    jsPDFAPI.getTextDimensions = function (txt) {
+        fontName = this.internal.getFont().fontName;
+        fontSize = this.table_font_size || this.internal.getFontSize();
+        fontStyle = this.internal.getFont().fontStyle;
+        // 1 pixel = 0.264583 mm and 1 mm = 72/25.4 point
+        var px2pt = 0.264583 * 72 / 25.4,
+            dimensions,
+            text;
+
+        text = document.createElement('font');
+        text.id = "jsPDFCell";
+        text.style.fontStyle = fontStyle;
+        text.style.fontName = fontName;
+        text.style.fontSize = fontSize + 'pt';
+        text.textContent = txt;
+
+        document.body.appendChild(text);
+
+        dimensions = { w: (text.offsetWidth + 1) * px2pt, h: (text.offsetHeight + 1) * px2pt};
+
+        document.body.removeChild(text);
+
+        return dimensions;
+    };
+
+    jsPDFAPI.cellAddPage = function () {
+        var margins = this.margins || NO_MARGINS;
+
+        this.addPage();
+
+        setLastCellPosition(margins.left, margins.top, undefined, undefined);
+        //setLastCellPosition(undefined, undefined, undefined, undefined, undefined);
+        pages += 1;
+    };
+
+    jsPDFAPI.cellInitialize = function () {
+        lastCellPos = { x: undefined, y: undefined, w: undefined, h: undefined, ln: undefined };
+        pages = 1;
+    };
+
+    jsPDFAPI.cell = function (x, y, w, h, txt, ln, align) {
+        var curCell = getLastCellPosition();
+
+        // If this is not the first cell, we must change its position
+        if (curCell.ln !== undefined) {
+            if (curCell.ln === ln) {
+                //Same line
+                x = curCell.x + curCell.w;
+                y = curCell.y;
+            } else {
+                //New line
+                var margins = this.margins || NO_MARGINS;
+                if ((curCell.y + curCell.h + h + margin) >= this.internal.pageSize.height - margins.bottom) {
+                    this.cellAddPage();
+                    if (this.printHeaders && this.tableHeaderRow) {
+                        this.printHeaderRow(ln, true);
+                    }
+                }
+                //We ignore the passed y: the lines may have diferent heights
+                y = (getLastCellPosition().y + getLastCellPosition().h);
+
+            }
+        }
+
+        if (txt[0] !== undefined) {
+            if (this.printingHeaderRow) {
+                this.rect(x, y, w, h, 'FD');
+            } else {
+                this.rect(x, y, w, h);
+            }
+            if (align === 'right') {
+                if (txt instanceof Array) {
+                    for(var i = 0; i<txt.length; i++) {
+                        var currentLine = txt[i];
+                        var textSize = this.getStringUnitWidth(currentLine) * this.internal.getFontSize();
+                        this.text(currentLine, x + w - textSize - padding, y + this.internal.getLineHeight()*(i+1));
+                    }
+                }
+            } else {
+                this.text(txt, x + padding, y + this.internal.getLineHeight());
+            }
+        }
+        setLastCellPosition(x, y, w, h, ln);
+        return this;
+    };
+
+    /**
+     * Return the maximum value from an array
+     * @param array
+     * @param comparisonFn
+     * @returns {*}
+     */
+    jsPDFAPI.arrayMax = function (array, comparisonFn) {
+        var max = array[0],
+            i,
+            ln,
+            item;
+
+        for (i = 0, ln = array.length; i < ln; i += 1) {
+            item = array[i];
+
+            if (comparisonFn) {
+                if (comparisonFn(max, item) === -1) {
+                    max = item;
+                }
+            } else {
+                if (item > max) {
+                    max = item;
+                }
+            }
+        }
+
+        return max;
+    };
+
+    /**
+     * Create a table from a set of data.
+     * @param {Integer} [x] : left-position for top-left corner of table
+     * @param {Integer} [y] top-position for top-left corner of table
+     * @param {Object[]} [data] As array of objects containing key-value pairs corresponding to a row of data.
+     * @param {String[]} [headers] Omit or null to auto-generate headers at a performance cost
+
+     * @param {Object} [config.printHeaders] True to print column headers at the top of every page
+     * @param {Object} [config.autoSize] True to dynamically set the column widths to match the widest cell value
+     * @param {Object} [config.margins] margin values for left, top, bottom, and width
+     * @param {Object} [config.fontSize] Integer fontSize to use (optional)
+     */
+
+    jsPDFAPI.table = function (x,y, data, headers, config) {
+        if (!data) {
+            throw 'No data for PDF table';
+        }
+
+        var headerNames = [],
+            headerPrompts = [],
+            header,
+            i,
+            ln,
+            cln,
+            columnMatrix = {},
+            columnWidths = {},
+            columnData,
+            column,
+            columnMinWidths = [],
+            j,
+            tableHeaderConfigs = [],
+            model,
+            jln,
+            func,
+
+        //set up defaults. If a value is provided in config, defaults will be overwritten:
+           autoSize        = false,
+           printHeaders    = true,
+           fontSize        = 12,
+           margins         = NO_MARGINS;
+
+           margins.width = this.internal.pageSize.width;
+
+        if (config) {
+        //override config defaults if the user has specified non-default behavior:
+            if(config.autoSize === true) {
+                autoSize = true;
+            }
+            if(config.printHeaders === false) {
+                printHeaders = false;
+            }
+            if(config.fontSize){
+                fontSize = config.fontSize;
+            }
+            if(config.margins){
+                margins = config.margins;
+            }
+        }
+
+        /**
+         * @property {Number} lnMod
+         * Keep track of the current line number modifier used when creating cells
+         */
+        this.lnMod = 0;
+        lastCellPos = { x: undefined, y: undefined, w: undefined, h: undefined, ln: undefined },
+        pages = 1;
+
+        this.printHeaders = printHeaders;
+        this.margins = margins;
+        this.setFontSize(fontSize);
+        this.table_font_size = fontSize;
+
+        // Set header values
+        if (headers === undefined || (headers === null)) {
+            // No headers defined so we derive from data
+            headerNames = Object.keys(data[0]);
+
+        } else if (headers[0] && (typeof headers[0] !== 'string')) {
+            var px2pt = 0.264583 * 72 / 25.4;
+
+            // Split header configs into names and prompts
+            for (i = 0, ln = headers.length; i < ln; i += 1) {
+                header = headers[i];
+                headerNames.push(header.name);
+                headerPrompts.push(header.prompt);
+                columnWidths[header.name] = header.width *px2pt;
+            }
+
+        } else {
+            headerNames = headers;
+        }
+
+        if (autoSize) {
+            // Create a matrix of columns e.g., {column_title: [row1_Record, row2_Record]}
+            func = function (rec) {
+                return rec[header];
+            };
+
+            for (i = 0, ln = headerNames.length; i < ln; i += 1) {
+                header = headerNames[i];
+
+                columnMatrix[header] = data.map(
+                    func
+                );
+
+                // get header width
+                columnMinWidths.push(this.getTextDimensions(headerPrompts[i] || header).w);
+                column = columnMatrix[header];
+
+                // get cell widths
+                for (j = 0, cln = column.length; j < cln; j += 1) {
+                    columnData = column[j];
+                    columnMinWidths.push(this.getTextDimensions(columnData).w);
+                }
+
+                // get final column width
+                columnWidths[header] = jsPDFAPI.arrayMax(columnMinWidths);
+            }
+        }
+
+        // -- Construct the table
+
+        if (printHeaders) {
+            var lineHeight = this.calculateLineHeight(headerNames, columnWidths, headerPrompts.length?headerPrompts:headerNames);
+
+            // Construct the header row
+            for (i = 0, ln = headerNames.length; i < ln; i += 1) {
+                header = headerNames[i];
+                tableHeaderConfigs.push([x, y, columnWidths[header], lineHeight, String(headerPrompts.length ? headerPrompts[i] : header)]);
+            }
+
+            // Store the table header config
+            this.setTableHeaderRow(tableHeaderConfigs);
+
+            // Print the header for the start of the table
+            this.printHeaderRow(1, false);
+        }
+
+        // Construct the data rows
+        for (i = 0, ln = data.length; i < ln; i += 1) {
+            var lineHeight;
+            model = data[i];
+            lineHeight = this.calculateLineHeight(headerNames, columnWidths, model);
+
+            for (j = 0, jln = headerNames.length; j < jln; j += 1) {
+                header = headerNames[j];
+                this.cell(x, y, columnWidths[header], lineHeight, model[header], i + 2, header.align);
+            }
+        }
+        this.lastCellPos = lastCellPos;
+        this.table_x = x;
+        this.table_y = y;
+        return this;
+    };
+    /**
+     * Calculate the height for containing the highest column
+     * @param {String[]} headerNames is the header, used as keys to the data
+     * @param {Integer[]} columnWidths is size of each column
+     * @param {Object[]} model is the line of data we want to calculate the height of
+     */
+    jsPDFAPI.calculateLineHeight = function (headerNames, columnWidths, model) {
+        var header, lineHeight = 0;
+        for (var j = 0; j < headerNames.length; j++) {
+            header = headerNames[j];
+            model[header] = this.splitTextToSize(String(model[header]), columnWidths[header] - padding);
+            var h = this.internal.getLineHeight() * model[header].length + padding;
+            if (h > lineHeight)
+                lineHeight = h;
+        }
+        return lineHeight;
+    };
+
+    /**
+     * Store the config for outputting a table header
+     * @param {Object[]} config
+     * An array of cell configs that would define a header row: Each config matches the config used by jsPDFAPI.cell
+     * except the ln parameter is excluded
+     */
+    jsPDFAPI.setTableHeaderRow = function (config) {
+        this.tableHeaderRow = config;
+    };
+
+    /**
+     * Output the store header row
+     * @param lineNumber The line number to output the header at
+     */
+    jsPDFAPI.printHeaderRow = function (lineNumber, new_page) {
+        if (!this.tableHeaderRow) {
+            throw 'Property tableHeaderRow does not exist.';
+        }
+
+        var tableHeaderCell,
+            tmpArray,
+            i,
+            ln;
+
+        this.printingHeaderRow = true;
+        if (headerFunction !== undefined) {
+            var position = headerFunction(this, pages);
+            setLastCellPosition(position[0], position[1], position[2], position[3], -1);
+        }
+        this.setFontStyle('bold');
+        var tempHeaderConf = [];
+        for (i = 0, ln = this.tableHeaderRow.length; i < ln; i += 1) {
+            this.setFillColor(200,200,200);
+
+            tableHeaderCell = this.tableHeaderRow[i];
+            if (new_page) {
+                tableHeaderCell[1] = this.margins && this.margins.top || 0;
+                tempHeaderConf.push(tableHeaderCell);
+            }
+            tmpArray = [].concat(tableHeaderCell);
+            this.cell.apply(this, tmpArray.concat(lineNumber));
+        }
+        if (tempHeaderConf.length > 0){
+            this.setTableHeaderRow(tempHeaderConf);
+        }
+        this.setFontStyle('normal');
+        this.printingHeaderRow = false;
+    };
+
+})(jsPDF.API);
+/** @preserve
+ * jsPDF fromHTML plugin. BETA stage. API subject to change. Needs browser
+ * Copyright (c) 2012 Willow Systems Corporation, willow-systems.com
+ *               2014 Juan Pablo Gaviria, https://github.com/juanpgaviria
+ *               2014 Diego Casorran, https://github.com/diegocr
+ *               2014 Daniel Husar, https://github.com/danielhusar
+ *               2014 Wolfgang Gassler, https://github.com/woolfg
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ * ====================================================================
+ */
+
+(function (jsPDFAPI) {
+	var clone,
+	DrillForContent,
+	FontNameDB,
+	FontStyleMap,
+	FontWeightMap,
+	FloatMap,
+	ClearMap,
+	GetCSS,
+	PurgeWhiteSpace,
+	Renderer,
+	ResolveFont,
+	ResolveUnitedNumber,
+	UnitedNumberMap,
+	elementHandledElsewhere,
+	images,
+	loadImgs,
+	checkForFooter,
+	process,
+	tableToJson;
+	clone = (function () {
+		return function (obj) {
+			Clone.prototype = obj;
+			return new Clone()
+		};
+		function Clone() {}
+	})();
+	PurgeWhiteSpace = function (array) {
+		var fragment,
+		i,
+		l,
+		lTrimmed,
+		r,
+		rTrimmed,
+		trailingSpace;
+		i = 0;
+		l = array.length;
+		fragment = void 0;
+		lTrimmed = false;
+		rTrimmed = false;
+		while (!lTrimmed && i !== l) {
+			fragment = array[i] = array[i].trimLeft();
+			if (fragment) {
+				lTrimmed = true;
+			}
+			i++;
+		}
+		i = l - 1;
+		while (l && !rTrimmed && i !== -1) {
+			fragment = array[i] = array[i].trimRight();
+			if (fragment) {
+				rTrimmed = true;
+			}
+			i--;
+		}
+		r = /\s+$/g;
+		trailingSpace = true;
+		i = 0;
+		while (i !== l) {
+			fragment = array[i].replace(/\s+/g, " ");
+			if (trailingSpace) {
+				fragment = fragment.trimLeft();
+			}
+			if (fragment) {
+				trailingSpace = r.test(fragment);
+			}
+			array[i] = fragment;
+			i++;
+		}
+		return array;
+	};
+	Renderer = function (pdf, x, y, settings) {
+		this.pdf = pdf;
+		this.x = x;
+		this.y = y;
+		this.settings = settings;
+		//list of functions which are called after each element-rendering process
+		this.watchFunctions = [];
+		this.init();
+		return this;
+	};
+	ResolveFont = function (css_font_family_string) {
+		var name,
+		part,
+		parts;
+		name = void 0;
+		parts = css_font_family_string.split(",");
+		part = parts.shift();
+		while (!name && part) {
+			name = FontNameDB[part.trim().toLowerCase()];
+			part = parts.shift();
+		}
+		return name;
+	};
+	ResolveUnitedNumber = function (css_line_height_string) {
+
+		//IE8 issues
+		css_line_height_string = css_line_height_string === "auto" ? "0px" : css_line_height_string;
+		if (css_line_height_string.indexOf("em") > -1 && !isNaN(Number(css_line_height_string.replace("em", "")))) {
+			css_line_height_string = Number(css_line_height_string.replace("em", "")) * 18.719 + "px";
+		}
+		if (css_line_height_string.indexOf("pt") > -1 && !isNaN(Number(css_line_height_string.replace("pt", "")))) {
+			css_line_height_string = Number(css_line_height_string.replace("pt", "")) * 1.333 + "px";
+		}
+
+		var normal,
+		undef,
+		value;
+		undef = void 0;
+		normal = 16.00;
+		value = UnitedNumberMap[css_line_height_string];
+		if (value) {
+			return value;
+		}
+		value = {
+			"xx-small"  :  9,
+			"x-small"   : 11,
+			small       : 13,
+			medium      : 16,
+			large       : 19,
+			"x-large"   : 23,
+			"xx-large"  : 28,
+			auto        :  0
+		}[{ css_line_height_string : css_line_height_string }];
+
+		if (value !== undef) {
+			return UnitedNumberMap[css_line_height_string] = value / normal;
+		}
+		if (value = parseFloat(css_line_height_string)) {
+			return UnitedNumberMap[css_line_height_string] = value / normal;
+		}
+		value = css_line_height_string.match(/([\d\.]+)(px)/);
+		if (value.length === 3) {
+			return UnitedNumberMap[css_line_height_string] = parseFloat(value[1]) / normal;
+		}
+		return UnitedNumberMap[css_line_height_string] = 1;
+	};
+	GetCSS = function (element) {
+		var css,tmp,computedCSSElement;
+		computedCSSElement = (function (el) {
+			var compCSS;
+			compCSS = (function (el) {
+				if (document.defaultView && document.defaultView.getComputedStyle) {
+					return document.defaultView.getComputedStyle(el, null);
+				} else if (el.currentStyle) {
+					return el.currentStyle;
+				} else {
+					return el.style;
+				}
+			})(el);
+			return function (prop) {
+				prop = prop.replace(/-\D/g, function (match) {
+					return match.charAt(1).toUpperCase();
+				});
+				return compCSS[prop];
+			};
+		})(element);
+		css = {};
+		tmp = void 0;
+		css["font-family"] = ResolveFont(computedCSSElement("font-family")) || "times";
+		css["font-style"] = FontStyleMap[computedCSSElement("font-style")] || "normal";
+		css["text-align"] = TextAlignMap[computedCSSElement("text-align")] || "left";
+		tmp = FontWeightMap[computedCSSElement("font-weight")] || "normal";
+		if (tmp === "bold") {
+			if (css["font-style"] === "normal") {
+				css["font-style"] = tmp;
+			} else {
+				css["font-style"] = tmp + css["font-style"];
+			}
+		}
+		css["font-size"] = ResolveUnitedNumber(computedCSSElement("font-size")) || 1;
+		css["line-height"] = ResolveUnitedNumber(computedCSSElement("line-height")) || 1;
+		css["display"] = (computedCSSElement("display") === "inline" ? "inline" : "block");
+
+		tmp = (css["display"] === "block");
+		css["margin-top"]     = tmp && ResolveUnitedNumber(computedCSSElement("margin-top"))     || 0;
+		css["margin-bottom"]  = tmp && ResolveUnitedNumber(computedCSSElement("margin-bottom"))  || 0;
+		css["padding-top"]    = tmp && ResolveUnitedNumber(computedCSSElement("padding-top"))    || 0;
+		css["padding-bottom"] = tmp && ResolveUnitedNumber(computedCSSElement("padding-bottom")) || 0;
+		css["margin-left"]    = tmp && ResolveUnitedNumber(computedCSSElement("margin-left"))    || 0;
+		css["margin-right"]   = tmp && ResolveUnitedNumber(computedCSSElement("margin-right"))   || 0;
+		css["padding-left"]   = tmp && ResolveUnitedNumber(computedCSSElement("padding-left"))   || 0;
+		css["padding-right"]  = tmp && ResolveUnitedNumber(computedCSSElement("padding-right"))  || 0;
+
+		//float and clearing of floats
+		css["float"] = FloatMap[computedCSSElement("cssFloat")] || "none";
+		css["clear"] = ClearMap[computedCSSElement("clear")] || "none";
+		return css;
+	};
+	elementHandledElsewhere = function (element, renderer, elementHandlers) {
+		var handlers,
+		i,
+		isHandledElsewhere,
+		l,
+		t;
+		isHandledElsewhere = false;
+		i = void 0;
+		l = void 0;
+		t = void 0;
+		handlers = elementHandlers["#" + element.id];
+		if (handlers) {
+			if (typeof handlers === "function") {
+				isHandledElsewhere = handlers(element, renderer);
+			} else {
+				i = 0;
+				l = handlers.length;
+				while (!isHandledElsewhere && i !== l) {
+					isHandledElsewhere = handlers[i](element, renderer);
+					i++;
+				}
+			}
+		}
+		handlers = elementHandlers[element.nodeName];
+		if (!isHandledElsewhere && handlers) {
+			if (typeof handlers === "function") {
+				isHandledElsewhere = handlers(element, renderer);
+			} else {
+				i = 0;
+				l = handlers.length;
+				while (!isHandledElsewhere && i !== l) {
+					isHandledElsewhere = handlers[i](element, renderer);
+					i++;
+				}
+			}
+		}
+		return isHandledElsewhere;
+	};
+	tableToJson = function (table, renderer) {
+		var data,
+		headers,
+		i,
+		j,
+		rowData,
+		tableRow,
+		table_obj,
+		table_with,
+		cell,
+		l;
+		data = [];
+		headers = [];
+		i = 0;
+		l = table.rows[0].cells.length;
+		table_with = table.clientWidth;
+		while (i < l) {
+			cell = table.rows[0].cells[i];
+			headers[i] = {
+				name : cell.textContent.toLowerCase().replace(/\s+/g, ''),
+				prompt : cell.textContent.replace(/\r?\n/g, ''),
+				width : (cell.clientWidth / table_with) * renderer.pdf.internal.pageSize.width
+			};
+			i++;
+		}
+		i = 1;
+		while (i < table.rows.length) {
+			tableRow = table.rows[i];
+			rowData = {};
+			j = 0;
+			while (j < tableRow.cells.length) {
+				rowData[headers[j].name] = tableRow.cells[j].textContent.replace(/\r?\n/g, '');
+				j++;
+			}
+			data.push(rowData);
+			i++;
+		}
+		return table_obj = {
+			rows : data,
+			headers : headers
+		};
+	};
+	var SkipNode = {
+		SCRIPT   : 1,
+		STYLE    : 1,
+		NOSCRIPT : 1,
+		OBJECT   : 1,
+		EMBED    : 1,
+		SELECT   : 1
+	};
+	var listCount = 1;
+	DrillForContent = function (element, renderer, elementHandlers) {
+		var cn,
+		cns,
+		fragmentCSS,
+		i,
+		isBlock,
+		l,
+		px2pt,
+		table2json,
+		cb;
+		cns = element.childNodes;
+		cn = void 0;
+		fragmentCSS = GetCSS(element);
+		isBlock = fragmentCSS.display === "block";
+		if (isBlock) {
+			renderer.setBlockBoundary();
+			renderer.setBlockStyle(fragmentCSS);
+		}
+		px2pt = 0.264583 * 72 / 25.4;
+		i = 0;
+		l = cns.length;
+		while (i < l) {
+			cn = cns[i];
+			if (typeof cn === "object") {
+
+				//execute all watcher functions to e.g. reset floating
+				renderer.executeWatchFunctions(cn);
+
+				/*** HEADER rendering **/
+				if (cn.nodeType === 1 && cn.nodeName === 'HEADER') {
+					var header = cn;
+					//store old top margin
+					var oldMarginTop = renderer.pdf.margins_doc.top;
+					//subscribe for new page event and render header first on every page
+					renderer.pdf.internal.events.subscribe('addPage', function (pageInfo) {
+						//set current y position to old margin
+						renderer.y = oldMarginTop;
+						//render all child nodes of the header element
+						DrillForContent(header, renderer, elementHandlers);
+						//set margin to old margin + rendered header + 10 space to prevent overlapping
+						//important for other plugins (e.g. table) to start rendering at correct position after header
+						renderer.pdf.margins_doc.top = renderer.y + 10;
+						renderer.y += 10;
+					}, false);
+				}
+
+				if (cn.nodeType === 8 && cn.nodeName === "#comment") {
+					if (~cn.textContent.indexOf("ADD_PAGE")) {
+						renderer.pdf.addPage();
+						renderer.y = renderer.pdf.margins_doc.top;
+					}
+
+				} else if (cn.nodeType === 1 && !SkipNode[cn.nodeName]) {
+					/*** IMAGE RENDERING ***/
+					var cached_image;
+					if (cn.nodeName === "IMG") {
+						var url = cn.getAttribute("src");
+						cached_image = images[renderer.pdf.sHashCode(url) || url];
+					}
+					if (cached_image) {
+						if ((renderer.pdf.internal.pageSize.height - renderer.pdf.margins_doc.bottom < renderer.y + cn.height) && (renderer.y > renderer.pdf.margins_doc.top)) {
+							renderer.pdf.addPage();
+							renderer.y = renderer.pdf.margins_doc.top;
+							//check if we have to set back some values due to e.g. header rendering for new page
+							renderer.executeWatchFunctions(cn);
+						}
+
+						var imagesCSS = GetCSS(cn);
+						var imageX = renderer.x;
+						var fontToUnitRatio = 12 / renderer.pdf.internal.scaleFactor;
+
+						//define additional paddings, margins which have to be taken into account for margin calculations
+						var additionalSpaceLeft = (imagesCSS["margin-left"] + imagesCSS["padding-left"])*fontToUnitRatio;
+						var additionalSpaceRight = (imagesCSS["margin-right"] + imagesCSS["padding-right"])*fontToUnitRatio;
+						var additionalSpaceTop = (imagesCSS["margin-top"] + imagesCSS["padding-top"])*fontToUnitRatio;
+						var additionalSpaceBottom = (imagesCSS["margin-bottom"] + imagesCSS["padding-bottom"])*fontToUnitRatio;
+
+						//if float is set to right, move the image to the right border
+						//add space if margin is set
+						if (imagesCSS['float'] !== undefined && imagesCSS['float'] === 'right') {
+							imageX += renderer.settings.width - cn.width - additionalSpaceRight;
+						} else {
+							imageX +=  additionalSpaceLeft;
+						}
+
+						renderer.pdf.addImage(cached_image, imageX, renderer.y + additionalSpaceTop, cn.width, cn.height);
+						cached_image = undefined;
+						//if the float prop is specified we have to float the text around the image
+						if (imagesCSS['float'] === 'right' || imagesCSS['float'] === 'left') {
+							//add functiont to set back coordinates after image rendering
+							renderer.watchFunctions.push((function(diffX , thresholdY, diffWidth, el) {
+								//undo drawing box adaptions which were set by floating
+								if (renderer.y >= thresholdY) {
+									renderer.x += diffX;
+									renderer.settings.width += diffWidth;
+									return true;
+								} else if(el && el.nodeType === 1 && !SkipNode[el.nodeName] && renderer.x+el.width > (renderer.pdf.margins_doc.left + renderer.pdf.margins_doc.width)) {
+									renderer.x += diffX;
+									renderer.y = thresholdY;
+									renderer.settings.width += diffWidth;
+									return true;
+								} else {
+									return false;
+								}
+							}).bind(this, (imagesCSS['float'] === 'left') ? -cn.width-additionalSpaceLeft-additionalSpaceRight : 0, renderer.y+cn.height+additionalSpaceTop+additionalSpaceBottom, cn.width));
+							//reset floating by clear:both divs
+							//just set cursorY after the floating element
+							renderer.watchFunctions.push((function(yPositionAfterFloating, pages, el) {
+								if (renderer.y < yPositionAfterFloating && pages === renderer.pdf.internal.getNumberOfPages()) {
+									if (el.nodeType === 1 && GetCSS(el).clear === 'both') {
+										renderer.y = yPositionAfterFloating;
+										return true;
+									} else {
+										return false;
+									}
+								} else {
+									return true;
+								}
+							}).bind(this, renderer.y+cn.height, renderer.pdf.internal.getNumberOfPages()));
+
+							//if floating is set we decrease the available width by the image width
+							renderer.settings.width -= cn.width+additionalSpaceLeft+additionalSpaceRight;
+							//if left just add the image width to the X coordinate
+							if (imagesCSS['float'] === 'left') {
+								renderer.x += cn.width+additionalSpaceLeft+additionalSpaceRight;
+							}
+						} else {
+						//if no floating is set, move the rendering cursor after the image height
+							renderer.y += cn.height + additionalSpaceBottom;
+						}
+
+					/*** TABLE RENDERING ***/
+					} else if (cn.nodeName === "TABLE") {
+						table2json = tableToJson(cn, renderer);
+						renderer.y += 10;
+						renderer.pdf.table(renderer.x, renderer.y, table2json.rows, table2json.headers, {
+							autoSize : false,
+							printHeaders : true,
+							margins : renderer.pdf.margins_doc
+						});
+						renderer.y = renderer.pdf.lastCellPos.y + renderer.pdf.lastCellPos.h + 20;
+					} else if (cn.nodeName === "OL" || cn.nodeName === "UL") {
+						listCount = 1;
+						if (!elementHandledElsewhere(cn, renderer, elementHandlers)) {
+							DrillForContent(cn, renderer, elementHandlers);
+						}
+						renderer.y += 10;
+					} else if (cn.nodeName === "LI") {
+						var temp = renderer.x;
+						renderer.x += cn.parentNode.nodeName === "UL" ? 22 : 10;
+						renderer.y += 3;
+						if (!elementHandledElsewhere(cn, renderer, elementHandlers)) {
+							DrillForContent(cn, renderer, elementHandlers);
+						}
+						renderer.x = temp;
+					} else if (cn.nodeName === "BR") {
+						renderer.y += fragmentCSS["font-size"] * renderer.pdf.internal.scaleFactor;
+					} else {
+						if (!elementHandledElsewhere(cn, renderer, elementHandlers)) {
+							DrillForContent(cn, renderer, elementHandlers);
+						}
+					}
+				} else if (cn.nodeType === 3) {
+					var value = cn.nodeValue;
+					if (cn.nodeValue && cn.parentNode.nodeName === "LI") {
+						if (cn.parentNode.parentNode.nodeName === "OL") {
+							value = listCount++ + '. ' + value;
+						} else {
+							var fontPx = fragmentCSS["font-size"] * 16;
+							var radius = 2;
+							if (fontPx > 20) {
+								radius = 3;
+							}
+							cb = function (x, y) {
+								this.pdf.circle(x, y, radius, 'FD');
+							};
+						}
+					}
+					renderer.addText(value, fragmentCSS);
+				} else if (typeof cn === "string") {
+					renderer.addText(cn, fragmentCSS);
+				}
+			}
+			i++;
+		}
+
+		if (isBlock) {
+			return renderer.setBlockBoundary(cb);
+		}
+	};
+	images = {};
+	loadImgs = function (element, renderer, elementHandlers, cb) {
+		var imgs = element.getElementsByTagName('img'),
+		l = imgs.length, found_images,
+		x = 0;
+		function done() {
+			renderer.pdf.internal.events.publish('imagesLoaded');
+			cb(found_images);
+		}
+		function loadImage(url, width, height) {
+			if (!url)
+				return;
+			var img = new Image();
+			found_images = ++x;
+			img.crossOrigin = '';
+			img.onerror = img.onload = function () {
+				if(img.complete) {
+					//to support data urls in images, set width and height
+					//as those values are not recognized automatically
+					if (img.src.indexOf('data:image/') === 0) {
+						img.width = width || img.width || 0;
+						img.height = height || img.height || 0;
+					}
+					//if valid image add to known images array
+					if (img.width + img.height) {
+						var hash = renderer.pdf.sHashCode(url) || url;
+						images[hash] = images[hash] || img;
+					}
+				}
+				if(!--x) {
+					done();
+				}
+			};
+			img.src = url;
+		}
+		while (l--)
+			loadImage(imgs[l].getAttribute("src"),imgs[l].width,imgs[l].height);
+		return x || done();
+	};
+	checkForFooter = function (elem, renderer, elementHandlers) {
+		//check if we can found a <footer> element
+		var footer = elem.getElementsByTagName("footer");
+		if (footer.length > 0) {
+
+			footer = footer[0];
+
+			//bad hack to get height of footer
+			//creat dummy out and check new y after fake rendering
+			var oldOut = renderer.pdf.internal.write;
+			var oldY = renderer.y;
+			renderer.pdf.internal.write = function () {};
+			DrillForContent(footer, renderer, elementHandlers);
+			var footerHeight = Math.ceil(renderer.y - oldY) + 5;
+			renderer.y = oldY;
+			renderer.pdf.internal.write = oldOut;
+
+			//add 20% to prevent overlapping
+			renderer.pdf.margins_doc.bottom += footerHeight;
+
+			//Create function render header on every page
+			var renderFooter = function (pageInfo) {
+				var pageNumber = pageInfo !== undefined ? pageInfo.pageNumber : 1;
+				//set current y position to old margin
+				var oldPosition = renderer.y;
+				//render all child nodes of the header element
+				renderer.y = renderer.pdf.internal.pageSize.height - renderer.pdf.margins_doc.bottom;
+				renderer.pdf.margins_doc.bottom -= footerHeight;
+
+				//check if we have to add page numbers
+				var spans = footer.getElementsByTagName('span');
+				for (var i = 0; i < spans.length; ++i) {
+					//if we find some span element with class pageCounter, set the page
+					if ((" " + spans[i].className + " ").replace(/[\n\t]/g, " ").indexOf(" pageCounter ") > -1) {
+						spans[i].innerHTML = pageNumber;
+					}
+					//if we find some span element with class totalPages, set a variable which is replaced after rendering of all pages
+					if ((" " + spans[i].className + " ").replace(/[\n\t]/g, " ").indexOf(" totalPages ") > -1) {
+						spans[i].innerHTML = '###jsPDFVarTotalPages###';
+					}
+				}
+
+				//render footer content
+				DrillForContent(footer, renderer, elementHandlers);
+				//set bottom margin to previous height including the footer height
+				renderer.pdf.margins_doc.bottom += footerHeight;
+				//important for other plugins (e.g. table) to start rendering at correct position after header
+				renderer.y = oldPosition;
+			};
+
+			//check if footer contains totalPages which shoudl be replace at the disoposal of the document
+			var spans = footer.getElementsByTagName('span');
+			for (var i = 0; i < spans.length; ++i) {
+				if ((" " + spans[i].className + " ").replace(/[\n\t]/g, " ").indexOf(" totalPages ") > -1) {
+					renderer.pdf.internal.events.subscribe('htmlRenderingFinished', renderer.pdf.putTotalPages.bind(renderer.pdf, '###jsPDFVarTotalPages###'), true);
+				}
+			}
+
+			//register event to render footer on every new page
+			renderer.pdf.internal.events.subscribe('addPage', renderFooter, false);
+			//render footer on first page
+			renderFooter();
+
+			//prevent footer rendering
+			SkipNode['FOOTER'] = 1;
+		}
+	};
+	process = function (pdf, element, x, y, settings, callback) {
+		if (!element)
+			return false;
+		if (typeof element !== "string" && !element.parentNode)
+			element = '' + element.innerHTML;
+		if (typeof element === "string") {
+			element = (function (element) {
+				var $frame,
+				$hiddendiv,
+				framename,
+				visuallyhidden;
+				framename = "jsPDFhtmlText" + Date.now().toString() + (Math.random() * 1000).toFixed(0);
+				visuallyhidden = "position: absolute !important;" + "clip: rect(1px 1px 1px 1px); /* IE6, IE7 */" + "clip: rect(1px, 1px, 1px, 1px);" + "padding:0 !important;" + "border:0 !important;" + "height: 1px !important;" + "width: 1px !important; " + "top:auto;" + "left:-100px;" + "overflow: hidden;";
+				$hiddendiv = document.createElement('div');
+				$hiddendiv.style.cssText = visuallyhidden;
+				$hiddendiv.innerHTML = "<iframe style=\"height:1px;width:1px\" name=\"" + framename + "\" />";
+				document.body.appendChild($hiddendiv);
+				$frame = window.frames[framename];
+				$frame.document.body.innerHTML = element;
+				return $frame.document.body;
+			})(element.replace(/<\/?script[^>]*?>/gi, ''));
+		}
+		var r = new Renderer(pdf, x, y, settings), out;
+
+		// 1. load images
+		// 2. prepare optional footer elements
+		// 3. render content
+		loadImgs.call(this, element, r, settings.elementHandlers, function (found_images) {
+			checkForFooter( element, r, settings.elementHandlers);
+			DrillForContent(element, r, settings.elementHandlers);
+			//send event dispose for final taks (e.g. footer totalpage replacement)
+			r.pdf.internal.events.publish('htmlRenderingFinished');
+			out = r.dispose();
+			if (typeof callback === 'function') callback(out);
+			else if (found_images) console.error('jsPDF Warning: rendering issues? provide a callback to fromHTML!');
+		});
+		return out || {x: r.x, y:r.y};
+	};
+	Renderer.prototype.init = function () {
+		this.paragraph = {
+			text : [],
+			style : []
+		};
+		return this.pdf.internal.write("q");
+	};
+	Renderer.prototype.dispose = function () {
+		this.pdf.internal.write("Q");
+		return {
+			x : this.x,
+			y : this.y,
+			ready:true
+		};
+	};
+
+	//Checks if we have to execute some watcher functions
+	//e.g. to end text floating around an image
+	Renderer.prototype.executeWatchFunctions = function(el) {
+		var ret = false;
+		var narray = [];
+		if (this.watchFunctions.length > 0) {
+			for(var i=0; i< this.watchFunctions.length; ++i) {
+				if (this.watchFunctions[i](el) === true) {
+					ret = true;
+				} else {
+					narray.push(this.watchFunctions[i]);
+				}
+			}
+			this.watchFunctions = narray;
+		}
+		return ret;
+	};
+
+	Renderer.prototype.splitFragmentsIntoLines = function (fragments, styles) {
+		var currentLineLength,
+		defaultFontSize,
+		ff,
+		fontMetrics,
+		fontMetricsCache,
+		fragment,
+		fragmentChopped,
+		fragmentLength,
+		fragmentSpecificMetrics,
+		fs,
+		k,
+		line,
+		lines,
+		maxLineLength,
+		style;
+		defaultFontSize = 12;
+		k = this.pdf.internal.scaleFactor;
+		fontMetricsCache = {};
+		ff = void 0;
+		fs = void 0;
+		fontMetrics = void 0;
+		fragment = void 0;
+		style = void 0;
+		fragmentSpecificMetrics = void 0;
+		fragmentLength = void 0;
+		fragmentChopped = void 0;
+		line = [];
+		lines = [line];
+		currentLineLength = 0;
+		maxLineLength = this.settings.width;
+		while (fragments.length) {
+			fragment = fragments.shift();
+			style = styles.shift();
+			if (fragment) {
+				ff = style["font-family"];
+				fs = style["font-style"];
+				fontMetrics = fontMetricsCache[ff + fs];
+				if (!fontMetrics) {
+					fontMetrics = this.pdf.internal.getFont(ff, fs).metadata.Unicode;
+					fontMetricsCache[ff + fs] = fontMetrics;
+				}
+				fragmentSpecificMetrics = {
+					widths : fontMetrics.widths,
+					kerning : fontMetrics.kerning,
+					fontSize : style["font-size"] * defaultFontSize,
+					textIndent : currentLineLength
+				};
+				fragmentLength = this.pdf.getStringUnitWidth(fragment, fragmentSpecificMetrics) * fragmentSpecificMetrics.fontSize / k;
+				if (currentLineLength + fragmentLength > maxLineLength) {
+					fragmentChopped = this.pdf.splitTextToSize(fragment, maxLineLength, fragmentSpecificMetrics);
+					line.push([fragmentChopped.shift(), style]);
+					while (fragmentChopped.length) {
+						line = [[fragmentChopped.shift(), style]];
+						lines.push(line);
+					}
+					currentLineLength = this.pdf.getStringUnitWidth(line[0][0], fragmentSpecificMetrics) * fragmentSpecificMetrics.fontSize / k;
+				} else {
+					line.push([fragment, style]);
+					currentLineLength += fragmentLength;
+				}
+			}
+		}
+
+		//if text alignment was set, set margin/indent of each line
+		if (style['text-align'] !== undefined && (style['text-align'] === 'center' || style['text-align'] === 'right' || style['text-align'] === 'justify')) {
+			for (var i = 0; i < lines.length; ++i) {
+				var length = this.pdf.getStringUnitWidth(lines[i][0][0], fragmentSpecificMetrics) * fragmentSpecificMetrics.fontSize / k;
+				//if there is more than on line we have to clone the style object as all lines hold a reference on this object
+				if (i > 0) {
+					lines[i][0][1] = clone(lines[i][0][1]);
+				}
+				var space = (maxLineLength - length);
+
+				if (style['text-align'] === 'right') {
+					lines[i][0][1]['margin-left'] = space;
+					//if alignment is not right, it has to be center so split the space to the left and the right
+				} else if (style['text-align'] === 'center') {
+					lines[i][0][1]['margin-left'] = space / 2;
+					//if justify was set, calculate the word spacing and define in by using the css property
+				} else if (style['text-align'] === 'justify') {
+					var countSpaces = lines[i][0][0].split(' ').length - 1;
+					lines[i][0][1]['word-spacing'] = space / countSpaces;
+					//ignore the last line in justify mode
+					if (i === (lines.length - 1)) {
+						lines[i][0][1]['word-spacing'] = 0;
+					}
+				}
+			}
+		}
+
+		return lines;
+	};
+	Renderer.prototype.RenderTextFragment = function (text, style) {
+		var defaultFontSize,
+		font,
+		maxLineHeight;
+
+		maxLineHeight = 0;
+		defaultFontSize = 12;
+
+		if (this.pdf.internal.pageSize.height - this.pdf.margins_doc.bottom < this.y + this.pdf.internal.getFontSize()) {
+			this.pdf.internal.write("ET", "Q");
+			this.pdf.addPage();
+			this.y = this.pdf.margins_doc.top;
+			this.pdf.internal.write("q", "BT 0 g", this.pdf.internal.getCoordinateString(this.x), this.pdf.internal.getVerticalCoordinateString(this.y), "Td");
+			//move cursor by one line on new page
+			maxLineHeight = Math.max(maxLineHeight, style["line-height"], style["font-size"]);
+			this.pdf.internal.write(0, (-1 * defaultFontSize * maxLineHeight).toFixed(2), "Td");
+		}
+
+		font = this.pdf.internal.getFont(style["font-family"], style["font-style"]);
+
+		//set the word spacing for e.g. justify style
+		if (style['word-spacing'] !== undefined && style['word-spacing'] > 0) {
+			this.pdf.internal.write(style['word-spacing'].toFixed(2), "Tw");
+		}
+
+		this.pdf.internal.write("/" + font.id, (defaultFontSize * style["font-size"]).toFixed(2), "Tf", "(" + this.pdf.internal.pdfEscape(text) + ") Tj");
+
+		//set the word spacing back to neutral => 0
+		if (style['word-spacing'] !== undefined) {
+			this.pdf.internal.write(0, "Tw");
+		}
+	};
+	Renderer.prototype.renderParagraph = function (cb) {
+		var blockstyle,
+		defaultFontSize,
+		fontToUnitRatio,
+		fragments,
+		i,
+		l,
+		line,
+		lines,
+		maxLineHeight,
+		out,
+		paragraphspacing_after,
+		paragraphspacing_before,
+		priorblockstype,
+		styles,
+		fontSize;
+		fragments = PurgeWhiteSpace(this.paragraph.text);
+		styles = this.paragraph.style;
+		blockstyle = this.paragraph.blockstyle;
+		priorblockstype = this.paragraph.blockstyle || {};
+		this.paragraph = {
+			text : [],
+			style : [],
+			blockstyle : {},
+			priorblockstyle : blockstyle
+		};
+		if (!fragments.join("").trim()) {
+			return;
+		}
+		lines = this.splitFragmentsIntoLines(fragments, styles);
+		line = void 0;
+		maxLineHeight = void 0;
+		defaultFontSize = 12;
+		fontToUnitRatio = defaultFontSize / this.pdf.internal.scaleFactor;
+		paragraphspacing_before = (Math.max((blockstyle["margin-top"] || 0) - (priorblockstype["margin-bottom"] || 0), 0) + (blockstyle["padding-top"] || 0)) * fontToUnitRatio;
+		paragraphspacing_after = ((blockstyle["margin-bottom"] || 0) + (blockstyle["padding-bottom"] || 0)) * fontToUnitRatio;
+		out = this.pdf.internal.write;
+		i = void 0;
+		l = void 0;
+		this.y += paragraphspacing_before;
+		out("q", "BT 0 g", this.pdf.internal.getCoordinateString(this.x), this.pdf.internal.getVerticalCoordinateString(this.y), "Td");
+
+		//stores the current indent of cursor position
+		var currentIndent = 0;
+
+		while (lines.length) {
+			line = lines.shift();
+			maxLineHeight = 0;
+			i = 0;
+			l = line.length;
+			while (i !== l) {
+				if (line[i][0].trim()) {
+					maxLineHeight = Math.max(maxLineHeight, line[i][1]["line-height"], line[i][1]["font-size"]);
+					fontSize = line[i][1]["font-size"] * 7;
+				}
+				i++;
+			}
+			//if we have to move the cursor to adapt the indent
+			var indentMove = 0;
+			//if a margin was added (by e.g. a text-alignment), move the cursor
+			if (line[0][1]["margin-left"] !== undefined && line[0][1]["margin-left"] > 0) {
+				wantedIndent = this.pdf.internal.getCoordinateString(line[0][1]["margin-left"]);
+				indentMove = wantedIndent - currentIndent;
+				currentIndent = wantedIndent;
+			}
+			//move the cursor
+			out(indentMove, (-1 * defaultFontSize * maxLineHeight).toFixed(2), "Td");
+			i = 0;
+			l = line.length;
+			while (i !== l) {
+				if (line[i][0]) {
+					this.RenderTextFragment(line[i][0], line[i][1]);
+				}
+				i++;
+			}
+			this.y += maxLineHeight * fontToUnitRatio;
+
+			//if some watcher function was executed sucessful, so e.g. margin and widths were changed,
+			//reset line drawing and calculate position and lines again
+			//e.g. to stop text floating around an image
+			if (this.executeWatchFunctions(line[0][1]) && lines.length > 0) {
+				var localFragments = [];
+				var localStyles = [];
+				//create fragement array of
+				lines.forEach(function(localLine) {
+					var i = 0;
+					var l = localLine.length;
+					while (i !== l) {
+						if (localLine[i][0]) {
+							localFragments.push(localLine[i][0]+' ');
+							localStyles.push(localLine[i][1]);
+						}
+						++i;
+					}
+				});
+				//split lines again due to possible coordinate changes
+				lines = this.splitFragmentsIntoLines(PurgeWhiteSpace(localFragments), localStyles);
+				//reposition the current cursor
+				out("ET", "Q");
+				out("q", "BT 0 g", this.pdf.internal.getCoordinateString(this.x), this.pdf.internal.getVerticalCoordinateString(this.y), "Td");
+			}
+
+		}
+		if (cb && typeof cb === "function") {
+			cb.call(this, this.x - 9, this.y - fontSize / 2);
+		}
+		out("ET", "Q");
+		return this.y += paragraphspacing_after;
+	};
+	Renderer.prototype.setBlockBoundary = function (cb) {
+		return this.renderParagraph(cb);
+	};
+	Renderer.prototype.setBlockStyle = function (css) {
+		return this.paragraph.blockstyle = css;
+	};
+	Renderer.prototype.addText = function (text, css) {
+		this.paragraph.text.push(text);
+		return this.paragraph.style.push(css);
+	};
+	FontNameDB = {
+		helvetica         : "helvetica",
+		"sans-serif"      : "helvetica",
+		"times new roman" : "times",
+		serif             : "times",
+		times             : "times",
+		monospace         : "courier",
+		courier           : "courier"
+	};
+	FontWeightMap = {
+		100 : "normal",
+		200 : "normal",
+		300 : "normal",
+		400 : "normal",
+		500 : "bold",
+		600 : "bold",
+		700 : "bold",
+		800 : "bold",
+		900 : "bold",
+		normal  : "normal",
+		bold    : "bold",
+		bolder  : "bold",
+		lighter : "normal"
+	};
+	FontStyleMap = {
+		normal  : "normal",
+		italic  : "italic",
+		oblique : "italic"
+	};
+	TextAlignMap = {
+		left    : "left",
+		right   : "right",
+		center  : "center",
+		justify : "justify"
+	};
+	FloatMap = {
+		none : 'none',
+		right: 'right',
+		left: 'left'
+	};
+	ClearMap = {
+	  none : 'none',
+	  both : 'both'
+	};
+	UnitedNumberMap = {
+		normal : 1
+	};
+	/**
+	 * Converts HTML-formatted text into formatted PDF text.
+	 *
+	 * Notes:
+	 * 2012-07-18
+	 * Plugin relies on having browser, DOM around. The HTML is pushed into dom and traversed.
+	 * Plugin relies on jQuery for CSS extraction.
+	 * Targeting HTML output from Markdown templating, which is a very simple
+	 * markup - div, span, em, strong, p. No br-based paragraph separation supported explicitly (but still may work.)
+	 * Images, tables are NOT supported.
+	 *
+	 * @public
+	 * @function
+	 * @param HTML {String or DOM Element} HTML-formatted text, or pointer to DOM element that is to be rendered into PDF.
+	 * @param x {Number} starting X coordinate in jsPDF instance's declared units.
+	 * @param y {Number} starting Y coordinate in jsPDF instance's declared units.
+	 * @param settings {Object} Additional / optional variables controlling parsing, rendering.
+	 * @returns {Object} jsPDF instance
+	 */
+	jsPDFAPI.fromHTML = function (HTML, x, y, settings, callback, margins) {
+		"use strict";
+
+		this.margins_doc = margins || {
+			top : 0,
+			bottom : 0
+		};
+		if (!settings)
+			settings = {};
+		if (!settings.elementHandlers)
+			settings.elementHandlers = {};
+
+		return process(this, HTML, isNaN(x) ? 4 : x, isNaN(y) ? 4 : y, settings, callback);
+	};
+})(jsPDF.API);
+/** ==================================================================== 
+ * jsPDF JavaScript plugin
+ * Copyright (c) 2013 Youssef Beddad, youssef.beddad@gmail.com
+ * 
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ * ====================================================================
+ */
+
+/*global jsPDF */
+
+(function (jsPDFAPI) {
+    'use strict';
+    var jsNamesObj, jsJsObj, text;
+    jsPDFAPI.addJS = function (txt) {
+        text = txt;
+        this.internal.events.subscribe(
+            'postPutResources',
+            function (txt) {
+                jsNamesObj = this.internal.newObject();
+                this.internal.write('<< /Names [(EmbeddedJS) ' + (jsNamesObj + 1) + ' 0 R] >>', 'endobj');
+                jsJsObj = this.internal.newObject();
+                this.internal.write('<< /S /JavaScript /JS (', text, ') >>', 'endobj');
+            }
+        );
+        this.internal.events.subscribe(
+            'putCatalog',
+            function () {
+                if (jsNamesObj !== undefined && jsJsObj !== undefined) {
+                    this.internal.write('/Names <</JavaScript ' + jsNamesObj + ' 0 R>>');
+                }
+            }
+        );
+        return this;
+    };
+}(jsPDF.API));
+/**@preserve
+ *  ==================================================================== 
+ * jsPDF PNG PlugIn
+ * Copyright (c) 2014 James Robb, https://github.com/jamesbrobb
+ * 
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ * ====================================================================
+ */
+
+(function(jsPDFAPI) {
+'use strict'
+	
+	/*
+	 * @see http://www.w3.org/TR/PNG-Chunks.html
+	 * 
+	 Color    Allowed      Interpretation
+	 Type     Bit Depths
+	   
+	   0       1,2,4,8,16  Each pixel is a grayscale sample.
+	   
+	   2       8,16        Each pixel is an R,G,B triple.
+	   
+	   3       1,2,4,8     Each pixel is a palette index;
+	                       a PLTE chunk must appear.
+	   
+	   4       8,16        Each pixel is a grayscale sample,
+	                       followed by an alpha sample.
+	   
+	   6       8,16        Each pixel is an R,G,B triple,
+	                       followed by an alpha sample.
+	*/
+	
+	/*
+	 * PNG filter method types
+	 * 
+	 * @see http://www.w3.org/TR/PNG-Filters.html
+	 * @see http://www.libpng.org/pub/png/book/chapter09.html
+	 * 
+	 * This is what the value 'Predictor' in decode params relates to
+	 * 
+	 * 15 is "optimal prediction", which means the prediction algorithm can change from line to line.
+	 * In that case, you actually have to read the first byte off each line for the prediction algorthim (which should be 0-4, corresponding to PDF 10-14) and select the appropriate unprediction algorithm based on that byte.
+	 *
+	   0       None
+	   1       Sub
+	   2       Up
+	   3       Average
+	   4       Paeth
+	 */
+	
+	var doesNotHavePngJS = function() {
+		return typeof PNG !== 'function' || typeof FlateStream !== 'function';
+	}
+	, canCompress = function(value) {
+		return value !== jsPDFAPI.image_compression.NONE && hasCompressionJS();
+	}
+	, hasCompressionJS = function() {
+		var inst = typeof Deflater === 'function';
+		if(!inst)
+			throw new Error("requires deflate.js for compression")
+		return inst;
+	}
+	, compressBytes = function(bytes, lineLength, colorsPerPixel, compression) {
+		
+		var level = 5,
+			filter_method = filterUp;
+		
+		switch(compression) {
+		
+			case jsPDFAPI.image_compression.FAST:
+				
+				level = 3;
+				filter_method = filterSub;
+				break;
+				
+			case jsPDFAPI.image_compression.MEDIUM:
+				
+				level = 6;
+				filter_method = filterAverage;
+				break;
+				
+			case jsPDFAPI.image_compression.SLOW:
+				
+				level = 9;
+				filter_method = filterPaeth;//uses to sum to choose best filter for each line
+				break;
+		}
+		
+		bytes = applyPngFilterMethod(bytes, lineLength, colorsPerPixel, filter_method);
+		
+		var header = new Uint8Array(createZlibHeader(level));
+		var checksum = adler32(bytes);
+		
+		var deflate = new Deflater(level);
+		var a = deflate.append(bytes);
+		var cBytes = deflate.flush();
+		
+		var len = header.length + a.length + cBytes.length;
+		
+		var cmpd = new Uint8Array(len + 4);
+		cmpd.set(header);
+		cmpd.set(a, header.length);
+		cmpd.set(cBytes, header.length + a.length);
+		
+		cmpd[len++] = (checksum >>> 24) & 0xff;
+		cmpd[len++] = (checksum >>> 16) & 0xff;
+		cmpd[len++] = (checksum >>> 8) & 0xff;
+		cmpd[len++] = checksum & 0xff;
+		
+		return jsPDFAPI.arrayBufferToBinaryString(cmpd);
+	}
+	, createZlibHeader = function(bytes, level){
+		/*
+		 * @see http://www.ietf.org/rfc/rfc1950.txt for zlib header 
+		 */
+		var cm = 8;
+        var cinfo = Math.LOG2E * Math.log(0x8000) - 8;
+        var cmf = (cinfo << 4) | cm;
+        
+        var hdr = cmf << 8;
+        var flevel = Math.min(3, ((level - 1) & 0xff) >> 1);
+        
+        hdr |= (flevel << 6);
+        hdr |= 0;//FDICT
+        hdr += 31 - (hdr % 31);
+        
+        return [cmf, (hdr & 0xff) & 0xff];
+	}
+	, adler32 = function(array, param) {
+		var adler = 1;
+	    var s1 = adler & 0xffff,
+	        s2 = (adler >>> 16) & 0xffff;
+	    var len = array.length;
+	    var tlen;
+	    var i = 0;
+
+	    while (len > 0) {
+	      tlen = len > param ? param : len;
+	      len -= tlen;
+	      do {
+	        s1 += array[i++];
+	        s2 += s1;
+	      } while (--tlen);
+
+	      s1 %= 65521;
+	      s2 %= 65521;
+	    }
+
+	    return ((s2 << 16) | s1) >>> 0;
+	}
+	, applyPngFilterMethod = function(bytes, lineLength, colorsPerPixel, filter_method) {
+		var lines = bytes.length / lineLength,
+			result = new Uint8Array(bytes.length + lines),
+			filter_methods = getFilterMethods(),
+			i = 0, line, prevLine, offset;
+		
+		for(; i < lines; i++) {
+			offset = i * lineLength;
+			line = bytes.subarray(offset, offset + lineLength);
+			
+			if(filter_method) {
+				result.set(filter_method(line, colorsPerPixel, prevLine), offset + i);
+				
+			}else{
+			
+				var j = 0,
+					len = filter_methods.length,
+					results = [];
+				
+				for(; j < len; j++)
+					results[j] = filter_methods[j](line, colorsPerPixel, prevLine);
+				
+				var ind = getIndexOfSmallestSum(results.concat());
+				
+				result.set(results[ind], offset + i);
+			}
+			
+			prevLine = line;
+		}
+		
+		return result;
+	}
+	, filterNone = function(line, colorsPerPixel, prevLine) {
+		/*var result = new Uint8Array(line.length + 1);
+		result[0] = 0;
+		result.set(line, 1);*/
+		
+		var result = Array.apply([], line);
+		result.unshift(0);
+
+		return result;
+	}
+	, filterSub = function(line, colorsPerPixel, prevLine) {
+		var result = [],
+			i = 0,
+			len = line.length,
+			left;
+		
+		result[0] = 1;
+		
+		for(; i < len; i++) {
+			left = line[i - colorsPerPixel] || 0;
+			result[i + 1] = (line[i] - left + 0x0100) & 0xff;
+		}
+		
+		return result;
+	}
+	, filterUp = function(line, colorsPerPixel, prevLine) {
+		var result = [],
+			i = 0,
+			len = line.length,
+			up;
+		
+		result[0] = 2;
+		
+		for(; i < len; i++) {
+			up = prevLine && prevLine[i] || 0;
+			result[i + 1] = (line[i] - up + 0x0100) & 0xff;
+		}
+		
+		return result;
+	}
+	, filterAverage = function(line, colorsPerPixel, prevLine) {
+		var result = [],
+			i = 0,
+			len = line.length,
+			left,
+			up;
+	
+		result[0] = 3;
+		
+		for(; i < len; i++) {
+			left = line[i - colorsPerPixel] || 0;
+			up = prevLine && prevLine[i] || 0;
+			result[i + 1] = (line[i] + 0x0100 - ((left + up) >>> 1)) & 0xff;
+		}
+		
+		return result;
+	}
+	, filterPaeth = function(line, colorsPerPixel, prevLine) {
+		var result = [],
+			i = 0,
+			len = line.length,
+			left,
+			up,
+			upLeft,
+			paeth;
+		
+		result[0] = 4;
+		
+		for(; i < len; i++) {
+			left = line[i - colorsPerPixel] || 0;
+			up = prevLine && prevLine[i] || 0;
+			upLeft = prevLine && prevLine[i - colorsPerPixel] || 0;
+			paeth = paethPredictor(left, up, upLeft);
+			result[i + 1] = (line[i] - paeth + 0x0100) & 0xff;
+		}
+		
+		return result;
+	}
+	,paethPredictor = function(left, up, upLeft) {
+
+		var p = left + up - upLeft,
+	        pLeft = Math.abs(p - left),
+	        pUp = Math.abs(p - up),
+	        pUpLeft = Math.abs(p - upLeft);
+		
+		return (pLeft <= pUp && pLeft <= pUpLeft) ? left : (pUp <= pUpLeft) ? up : upLeft;
+	}
+	, getFilterMethods = function() {
+		return [filterNone, filterSub, filterUp, filterAverage, filterPaeth];
+	}
+	,getIndexOfSmallestSum = function(arrays) {
+		var i = 0,
+			len = arrays.length,
+			sum, min, ind;
+		
+		while(i < len) {
+			sum = absSum(arrays[i].slice(1));
+			
+			if(sum < min || !min) {
+				min = sum;
+				ind = i;
+			}
+			
+			i++;
+		}
+		
+		return ind;
+	}
+	, absSum = function(array) {
+		var i = 0,
+			len = array.length,
+			sum = 0;
+	
+		while(i < len)
+			sum += Math.abs(array[i++]);
+			
+		return sum;
+	}
+	, logImg = function(img) {
+		console.log("width: " + img.width);
+		console.log("height: " + img.height);
+		console.log("bits: " + img.bits);
+		console.log("colorType: " + img.colorType);
+		console.log("transparency:");
+		console.log(img.transparency);
+		console.log("text:");
+		console.log(img.text);
+		console.log("compressionMethod: " + img.compressionMethod);
+		console.log("filterMethod: " + img.filterMethod);
+		console.log("interlaceMethod: " + img.interlaceMethod);
+		console.log("imgData:");
+		console.log(img.imgData);
+		console.log("palette:");
+		console.log(img.palette);
+		console.log("colors: " + img.colors);
+		console.log("colorSpace: " + img.colorSpace);
+		console.log("pixelBitlength: " + img.pixelBitlength);
+		console.log("hasAlphaChannel: " + img.hasAlphaChannel);
+	};
+	
+	
+	
+	
+	jsPDFAPI.processPNG = function(imageData, imageIndex, alias, compression, dataAsBinaryString) {
+		'use strict'
+		
+		var colorSpace = this.color_spaces.DEVICE_RGB,
+			decode = this.decode.FLATE_DECODE,
+			bpc = 8,
+			img, dp, trns,
+			colors, pal, smask;
+		
+	/*	if(this.isString(imageData)) {
+			
+		}*/
+		
+		if(this.isArrayBuffer(imageData))
+			imageData = new Uint8Array(imageData);
+		
+		if(this.isArrayBufferView(imageData)) {
+			
+			if(doesNotHavePngJS())
+				throw new Error("PNG support requires png.js and zlib.js");
+				
+			img = new PNG(imageData);
+			imageData = img.imgData;
+			bpc = img.bits;
+			colorSpace = img.colorSpace;
+			colors = img.colors;
+			
+			//logImg(img);
+			
+			/*
+			 * colorType 6 - Each pixel is an R,G,B triple, followed by an alpha sample.
+			 * 
+			 * colorType 4 - Each pixel is a grayscale sample, followed by an alpha sample.
+			 * 
+			 * Extract alpha to create two separate images, using the alpha as a sMask
+			 */
+			if([4,6].indexOf(img.colorType) !== -1) {
+				
+				/*
+				 * processes 8 bit RGBA and grayscale + alpha images
+				 */
+				if(img.bits === 8) {
+				
+					var pixelsArrayType = window['Uint' + img.pixelBitlength + 'Array'],
+						pixels = new pixelsArrayType(img.decodePixels().buffer),
+						len = pixels.length,
+						imgData = new Uint8Array(len * img.colors),
+						alphaData = new Uint8Array(len),
+						pDiff = img.pixelBitlength - img.bits,
+						i = 0, n = 0, pixel, pbl;
+				
+					for(; i < len; i++) {
+						pixel = pixels[i];
+						pbl = 0;
+						
+						while(pbl < pDiff) {
+							
+							imgData[n++] = ( pixel >>> pbl ) & 0xff;
+							pbl = pbl + img.bits;
+						}
+						
+						alphaData[i] = ( pixel >>> pbl ) & 0xff;
+					}
+				}
+				
+				/*
+				 * processes 16 bit RGBA and grayscale + alpha images
+				 */
+				if(img.bits === 16) {
+					
+					var pixels = new Uint32Array(img.decodePixels().buffer),
+						len = pixels.length,
+						imgData = new Uint8Array((len * (32 / img.pixelBitlength) ) * img.colors),
+						alphaData = new Uint8Array(len * (32 / img.pixelBitlength) ),
+						hasColors = img.colors > 1,
+						i = 0, n = 0, a = 0, pixel;
+					
+					while(i < len) {
+						pixel = pixels[i++];
+						
+						imgData[n++] = (pixel >>> 0) & 0xFF;
+						
+						if(hasColors) {
+							imgData[n++] = (pixel >>> 16) & 0xFF;
+							
+							pixel = pixels[i++];
+							imgData[n++] = (pixel >>> 0) & 0xFF;
+						}
+						
+						alphaData[a++] = (pixel >>> 16) & 0xFF;
+					}
+					
+					bpc = 8;
+				}
+				
+				if(canCompress(compression)) {
+										
+					imageData = compressBytes(imgData, img.width * img.colors, img.colors, compression);
+					smask = compressBytes(alphaData, img.width, 1, compression);
+					
+				}else{
+					
+					imageData = imgData;
+					smask = alphaData;
+					decode = null;
+				}
+			}
+			
+			/*
+			 * Indexed png. Each pixel is a palette index.
+			 */
+			if(img.colorType === 3) {
+				
+				colorSpace = this.color_spaces.INDEXED;
+				pal = img.palette;
+				
+				if(img.transparency.indexed) {
+					
+					var trans = img.transparency.indexed;
+					
+					var total = 0,
+						i = 0,
+						len = trans.length;
+
+					for(; i<len; ++i)
+					    total += trans[i];
+					
+					total = total / 255;
+					
+					/*
+					 * a single color is specified as 100% transparent (0),
+					 * so we set trns to use a /Mask with that index
+					 */
+					if(total === len - 1 && trans.indexOf(0) !== -1) {
+						trns = [trans.indexOf(0)];
+					
+					/*
+					 * there's more than one colour within the palette that specifies
+					 * a transparency value less than 255, so we unroll the pixels to create an image sMask
+					 */
+					}else if(total !== len){
+						
+						var pixels = img.decodePixels(),
+							alphaData = new Uint8Array(pixels.length),
+							i = 0,
+							len = pixels.length;
+						
+						for(; i < len; i++)
+							alphaData[i] = trans[pixels[i]];
+						
+						smask = compressBytes(alphaData, img.width, 1);
+					}
+				}
+			}
+			
+			if(decode === this.decode.FLATE_DECODE)
+				dp = '/Predictor 15 /Colors '+ colors +' /BitsPerComponent '+ bpc +' /Columns '+ img.width;
+			else
+				//remove 'Predictor' as it applies to the type of png filter applied to its IDAT - we only apply with compression
+				dp = '/Colors '+ colors +' /BitsPerComponent '+ bpc +' /Columns '+ img.width;
+			
+			if(this.isArrayBuffer(imageData) || this.isArrayBufferView(imageData))
+				imageData = this.arrayBufferToBinaryString(imageData);
+			
+			if(smask && this.isArrayBuffer(smask) || this.isArrayBufferView(smask))
+				smask = this.arrayBufferToBinaryString(smask);
+			
+			return this.createImageInfo(imageData, img.width, img.height, colorSpace,
+										bpc, decode, imageIndex, alias, dp, trns, pal, smask);
+		}
+		
+		throw new Error("Unsupported PNG image data, try using JPEG instead.");
+	}
+
+})(jsPDF.API)
+/** @preserve
+jsPDF Silly SVG plugin
+Copyright (c) 2012 Willow Systems Corporation, willow-systems.com
+*/
+/**
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ * ====================================================================
+ */
+
+;(function(jsPDFAPI) {
+'use strict'
+
+/**
+Parses SVG XML and converts only some of the SVG elements into
+PDF elements.
+
+Supports:
+ paths
+
+@public
+@function
+@param
+@returns {Type}
+*/
+jsPDFAPI.addSVG = function(svgtext, x, y, w, h) {
+	// 'this' is _jsPDF object returned when jsPDF is inited (new jsPDF())
+
+	var undef
+
+	if (x === undef || y === undef) {
+		throw new Error("addSVG needs values for 'x' and 'y'");
+	}
+
+    function InjectCSS(cssbody, document) {
+        var styletag = document.createElement('style');
+        styletag.type = 'text/css';
+        if (styletag.styleSheet) {
+        	// ie
+            styletag.styleSheet.cssText = cssbody;
+        } else {
+        	// others
+            styletag.appendChild(document.createTextNode(cssbody));
+        }
+        document.getElementsByTagName("head")[0].appendChild(styletag);
+    }
+
+	function createWorkerNode(document){
+
+		var frameID = 'childframe' // Date.now().toString() + '_' + (Math.random() * 100).toString()
+		, frame = document.createElement('iframe')
+
+		InjectCSS(
+			'.jsPDF_sillysvg_iframe {display:none;position:absolute;}'
+			, document
+		)
+
+		frame.name = frameID
+		frame.setAttribute("width", 0)
+		frame.setAttribute("height", 0)
+		frame.setAttribute("frameborder", "0")
+		frame.setAttribute("scrolling", "no")
+		frame.setAttribute("seamless", "seamless")
+		frame.setAttribute("class", "jsPDF_sillysvg_iframe")
+		
+		document.body.appendChild(frame)
+
+		return frame
+	}
+
+	function attachSVGToWorkerNode(svgtext, frame){
+		var framedoc = ( frame.contentWindow || frame.contentDocument ).document
+		framedoc.write(svgtext)
+		framedoc.close()
+		return framedoc.getElementsByTagName('svg')[0]
+	}
+
+	function convertPathToPDFLinesArgs(path){
+		'use strict'
+		// we will use 'lines' method call. it needs:
+		// - starting coordinate pair
+		// - array of arrays of vector shifts (2-len for line, 6 len for bezier)
+		// - scale array [horizontal, vertical] ratios
+		// - style (stroke, fill, both)
+
+		var x = parseFloat(path[1])
+		, y = parseFloat(path[2])
+		, vectors = []
+		, position = 3
+		, len = path.length
+
+		while (position < len){
+			if (path[position] === 'c'){
+				vectors.push([
+					parseFloat(path[position + 1])
+					, parseFloat(path[position + 2])
+					, parseFloat(path[position + 3])
+					, parseFloat(path[position + 4])
+					, parseFloat(path[position + 5])
+					, parseFloat(path[position + 6])
+				])
+				position += 7
+			} else if (path[position] === 'l') {
+				vectors.push([
+					parseFloat(path[position + 1])
+					, parseFloat(path[position + 2])
+				])
+				position += 3
+			} else {
+				position += 1
+			}
+		}
+		return [x,y,vectors]
+	}
+
+	var workernode = createWorkerNode(document)
+	, svgnode = attachSVGToWorkerNode(svgtext, workernode)
+	, scale = [1,1]
+	, svgw = parseFloat(svgnode.getAttribute('width'))
+	, svgh = parseFloat(svgnode.getAttribute('height'))
+
+	if (svgw && svgh) {
+		// setting both w and h makes image stretch to size.
+		// this may distort the image, but fits your demanded size
+		if (w && h) {
+			scale = [w / svgw, h / svgh]
+		} 
+		// if only one is set, that value is set as max and SVG 
+		// is scaled proportionately.
+		else if (w) {
+			scale = [w / svgw, w / svgw]
+		} else if (h) {
+			scale = [h / svgh, h / svgh]
+		}
+	}
+
+	var i, l, tmp
+	, linesargs
+	, items = svgnode.childNodes
+	for (i = 0, l = items.length; i < l; i++) {
+		tmp = items[i]
+		if (tmp.tagName && tmp.tagName.toUpperCase() === 'PATH') {
+			linesargs = convertPathToPDFLinesArgs( tmp.getAttribute("d").split(' ') )
+			// path start x coordinate
+			linesargs[0] = linesargs[0] * scale[0] + x // where x is upper left X of image
+			// path start y coordinate
+			linesargs[1] = linesargs[1] * scale[1] + y // where y is upper left Y of image
+			// the rest of lines are vectors. these will adjust with scale value auto.
+			this.lines.call(
+				this
+				, linesargs[2] // lines
+				, linesargs[0] // starting x
+				, linesargs[1] // starting y
+				, scale
+			)
+		}
+	}
+
+	// clean up
+	// workernode.parentNode.removeChild(workernode)
+
+	return this
+}
+
+})(jsPDF.API);
+/** @preserve
+ * jsPDF split_text_to_size plugin - MIT license.
+ * Copyright (c) 2012 Willow Systems Corporation, willow-systems.com
+ *               2014 Diego Casorran, https://github.com/diegocr
+ */
+/**
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ * ====================================================================
+ */
+
+;(function(API) {
+'use strict'
+
+/**
+Returns an array of length matching length of the 'word' string, with each
+cell ocupied by the width of the char in that position.
+
+@function
+@param word {String}
+@param widths {Object}
+@param kerning {Object}
+@returns {Array}
+*/
+var getCharWidthsArray = API.getCharWidthsArray = function(text, options){
+
+	if (!options) {
+		options = {}
+	}
+
+	var widths = options.widths ? options.widths : this.internal.getFont().metadata.Unicode.widths
+	, widthsFractionOf = widths.fof ? widths.fof : 1
+	, kerning = options.kerning ? options.kerning : this.internal.getFont().metadata.Unicode.kerning
+	, kerningFractionOf = kerning.fof ? kerning.fof : 1
+
+	// console.log("widths, kergnings", widths, kerning)
+
+	var i, l
+	, char_code
+	, prior_char_code = 0 // for kerning
+	, default_char_width = widths[0] || widthsFractionOf
+	, output = []
+
+	for (i = 0, l = text.length; i < l; i++) {
+		char_code = text.charCodeAt(i)
+		output.push(
+			( widths[char_code] || default_char_width ) / widthsFractionOf +
+			( kerning[char_code] && kerning[char_code][prior_char_code] || 0 ) / kerningFractionOf
+		)
+		prior_char_code = char_code
+	}
+
+	return output
+}
+var getArraySum = function(array){
+	var i = array.length
+	, output = 0
+	while(i){
+		;i--;
+		output += array[i]
+	}
+	return output
+}
+/**
+Returns a widths of string in a given font, if the font size is set as 1 point.
+
+In other words, this is "proportional" value. For 1 unit of font size, the length
+of the string will be that much.
+
+Multiply by font size to get actual width in *points*
+Then divide by 72 to get inches or divide by (72/25.6) to get 'mm' etc.
+
+@public
+@function
+@param
+@returns {Type}
+*/
+var getStringUnitWidth = API.getStringUnitWidth = function(text, options) {
+	return getArraySum(getCharWidthsArray.call(this, text, options))
+}
+
+/**
+returns array of lines
+*/
+var splitLongWord = function(word, widths_array, firstLineMaxLen, maxLen){
+	var answer = []
+
+	// 1st, chop off the piece that can fit on the hanging line.
+	var i = 0
+	, l = word.length
+	, workingLen = 0
+	while (i !== l && workingLen + widths_array[i] < firstLineMaxLen){
+		workingLen += widths_array[i]
+		;i++;
+	}
+	// this is first line.
+	answer.push(word.slice(0, i))
+
+	// 2nd. Split the rest into maxLen pieces.
+	var startOfLine = i
+	workingLen = 0
+	while (i !== l){
+		if (workingLen + widths_array[i] > maxLen) {
+			answer.push(word.slice(startOfLine, i))
+			workingLen = 0
+			startOfLine = i
+		}
+		workingLen += widths_array[i]
+		;i++;
+	}
+	if (startOfLine !== i) {
+		answer.push(word.slice(startOfLine, i))
+	}
+
+	return answer
+}
+
+// Note, all sizing inputs for this function must be in "font measurement units"
+// By default, for PDF, it's "point".
+var splitParagraphIntoLines = function(text, maxlen, options){
+	// at this time works only on Western scripts, ones with space char
+	// separating the words. Feel free to expand.
+
+	if (!options) {
+		options = {}
+	}
+
+	var line = []
+	, lines = [line]
+	, line_length = options.textIndent || 0
+	, separator_length = 0
+	, current_word_length = 0
+	, word
+	, widths_array
+	, words = text.split(' ')
+	, spaceCharWidth = getCharWidthsArray(' ', options)[0]
+	, i, l, tmp, lineIndent
+
+	if(options.lineIndent === -1) {
+		lineIndent = words[0].length +2;
+	} else {
+		lineIndent = options.lineIndent || 0;
+	}
+	if(lineIndent) {
+		var pad = Array(lineIndent).join(" "), wrds = [];
+		words.map(function(wrd) {
+			wrd = wrd.split(/\s*\n/);
+			if(wrd.length > 1) {
+				wrds = wrds.concat(wrd.map(function(wrd, idx) {
+					return (idx && wrd.length ? "\n":"") + wrd;
+				}));
+			} else {
+				wrds.push(wrd[0]);
+			}
+		});
+		words = wrds;
+		lineIndent = getStringUnitWidth(pad, options);
+	}
+
+	for (i = 0, l = words.length; i < l; i++) {
+		var force = 0;
+
+		word = words[i]
+		if(lineIndent && word[0] == "\n") {
+			word = word.substr(1);
+			force = 1;
+		}
+		widths_array = getCharWidthsArray(word, options)
+		current_word_length = getArraySum(widths_array)
+
+		if (line_length + separator_length + current_word_length > maxlen || force) {
+			if (current_word_length > maxlen) {
+				// this happens when you have space-less long URLs for example.
+				// we just chop these to size. We do NOT insert hiphens
+				tmp = splitLongWord(word, widths_array, maxlen - (line_length + separator_length), maxlen)
+				// first line we add to existing line object
+				line.push(tmp.shift()) // it's ok to have extra space indicator there
+				// last line we make into new line object
+				line = [tmp.pop()]
+				// lines in the middle we apped to lines object as whole lines
+				while(tmp.length){
+					lines.push([tmp.shift()]) // single fragment occupies whole line
+				}
+				current_word_length = getArraySum( widths_array.slice(word.length - line[0].length) )
+			} else {
+				// just put it on a new line
+				line = [word]
+			}
+
+			// now we attach new line to lines
+			lines.push(line)
+			line_length = current_word_length + lineIndent
+			separator_length = spaceCharWidth
+
+		} else {
+			line.push(word)
+
+			line_length += separator_length + current_word_length
+			separator_length = spaceCharWidth
+		}
+	}
+
+	if(lineIndent) {
+		var postProcess = function(ln, idx) {
+			return (idx ? pad : '') + ln.join(" ");
+		};
+	} else {
+		var postProcess = function(ln) { return ln.join(" ")};
+	}
+
+	return lines.map(postProcess);
+}
+
+/**
+Splits a given string into an array of strings. Uses 'size' value
+(in measurement units declared as default for the jsPDF instance)
+and the font's "widths" and "Kerning" tables, where availabe, to
+determine display length of a given string for a given font.
+
+We use character's 100% of unit size (height) as width when Width
+table or other default width is not available.
+
+@public
+@function
+@param text {String} Unencoded, regular JavaScript (Unicode, UTF-16 / UCS-2) string.
+@param size {Number} Nominal number, measured in units default to this instance of jsPDF.
+@param options {Object} Optional flags needed for chopper to do the right thing.
+@returns {Array} with strings chopped to size.
+*/
+API.splitTextToSize = function(text, maxlen, options) {
+	'use strict'
+
+	if (!options) {
+		options = {}
+	}
+
+	var fsize = options.fontSize || this.internal.getFontSize()
+	, newOptions = (function(options){
+		var widths = {0:1}
+		, kerning = {}
+
+		if (!options.widths || !options.kerning) {
+			var f = this.internal.getFont(options.fontName, options.fontStyle)
+			, encoding = 'Unicode'
+			// NOT UTF8, NOT UTF16BE/LE, NOT UCS2BE/LE
+			// Actual JavaScript-native String's 16bit char codes used.
+			// no multi-byte logic here
+
+			if (f.metadata[encoding]) {
+				return {
+					widths: f.metadata[encoding].widths || widths
+					, kerning: f.metadata[encoding].kerning || kerning
+				}
+			}
+		} else {
+			return 	{
+				widths: options.widths
+				, kerning: options.kerning
+			}
+		}
+
+		// then use default values
+		return 	{
+			widths: widths
+			, kerning: kerning
+		}
+	}).call(this, options)
+
+	// first we split on end-of-line chars
+	var paragraphs
+	if(Array.isArray(text)) {
+		paragraphs = text;
+	} else {
+		paragraphs = text.split(/\r?\n/);
+	}
+
+	// now we convert size (max length of line) into "font size units"
+	// at present time, the "font size unit" is always 'point'
+	// 'proportional' means, "in proportion to font size"
+	var fontUnit_maxLen = 1.0 * this.internal.scaleFactor * maxlen / fsize
+	// at this time, fsize is always in "points" regardless of the default measurement unit of the doc.
+	// this may change in the future?
+	// until then, proportional_maxlen is likely to be in 'points'
+
+	// If first line is to be indented (shorter or longer) than maxLen
+	// we indicate that by using CSS-style "text-indent" option.
+	// here it's in font units too (which is likely 'points')
+	// it can be negative (which makes the first line longer than maxLen)
+	newOptions.textIndent = options.textIndent ?
+		options.textIndent * 1.0 * this.internal.scaleFactor / fsize :
+		0
+	newOptions.lineIndent = options.lineIndent;
+
+	var i, l
+	, output = []
+	for (i = 0, l = paragraphs.length; i < l; i++) {
+		output = output.concat(
+			splitParagraphIntoLines(
+				paragraphs[i]
+				, fontUnit_maxLen
+				, newOptions
+			)
+		)
+	}
+
+	return output
+}
+
+})(jsPDF.API);
+/** @preserve 
+jsPDF standard_fonts_metrics plugin
+Copyright (c) 2012 Willow Systems Corporation, willow-systems.com
+MIT license.
+*/
+/**
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ * ====================================================================
+ */
+
+;(function(API) {
+'use strict'
+
+/*
+# reference (Python) versions of 'compress' and 'uncompress'
+# only 'uncompress' function is featured lower as JavaScript
+# if you want to unit test "roundtrip", just transcribe the reference
+# 'compress' function from Python into JavaScript
+
+def compress(data):
+
+	keys =   '0123456789abcdef'
+	values = 'klmnopqrstuvwxyz'
+	mapping = dict(zip(keys, values))
+	vals = []
+	for key in data.keys():
+		value = data[key]
+		try:
+			keystring = hex(key)[2:]
+			keystring = keystring[:-1] + mapping[keystring[-1:]]
+		except:
+			keystring = key.join(["'","'"])
+			#print('Keystring is %s' % keystring)
+
+		try:
+			if value < 0:
+				valuestring = hex(value)[3:]
+				numberprefix = '-'
+			else:
+				valuestring = hex(value)[2:]
+				numberprefix = ''
+			valuestring = numberprefix + valuestring[:-1] + mapping[valuestring[-1:]]
+		except:
+			if type(value) == dict:
+				valuestring = compress(value)
+			else:
+				raise Exception("Don't know what to do with value type %s" % type(value))
+
+		vals.append(keystring+valuestring)
+	
+	return '{' + ''.join(vals) + '}'
+
+def uncompress(data):
+
+	decoded = '0123456789abcdef'
+	encoded = 'klmnopqrstuvwxyz'
+	mapping = dict(zip(encoded, decoded))
+
+	sign = +1
+	stringmode = False
+	stringparts = []
+
+	output = {}
+
+	activeobject = output
+	parentchain = []
+
+	keyparts = ''
+	valueparts = ''
+
+	key = None
+
+	ending = set(encoded)
+
+	i = 1
+	l = len(data) - 1 # stripping starting, ending {}
+	while i != l: # stripping {}
+		# -, {, }, ' are special.
+
+		ch = data[i]
+		i += 1
+
+		if ch == "'":
+			if stringmode:
+				# end of string mode
+				stringmode = False
+				key = ''.join(stringparts)
+			else:
+				# start of string mode
+				stringmode = True
+				stringparts = []
+		elif stringmode == True:
+			#print("Adding %s to stringpart" % ch)
+			stringparts.append(ch)
+
+		elif ch == '{':
+			# start of object
+			parentchain.append( [activeobject, key] )
+			activeobject = {}
+			key = None
+			#DEBUG = True
+		elif ch == '}':
+			# end of object
+			parent, key = parentchain.pop()
+			parent[key] = activeobject
+			key = None
+			activeobject = parent
+			#DEBUG = False
+
+		elif ch == '-':
+			sign = -1
+		else:
+			# must be number
+			if key == None:
+				#debug("In Key. It is '%s', ch is '%s'" % (keyparts, ch))
+				if ch in ending:
+					#debug("End of key")
+					keyparts += mapping[ch]
+					key = int(keyparts, 16) * sign
+					sign = +1
+					keyparts = ''
+				else:
+					keyparts += ch
+			else:
+				#debug("In value. It is '%s', ch is '%s'" % (valueparts, ch))
+				if ch in ending:
+					#debug("End of value")
+					valueparts += mapping[ch]
+					activeobject[key] = int(valueparts, 16) * sign
+					sign = +1
+					key = None
+					valueparts = ''
+				else:
+					valueparts += ch
+
+			#debug(activeobject)
+
+	return output
+
+*/
+
+/**
+Uncompresses data compressed into custom, base16-like format. 
+@public
+@function
+@param
+@returns {Type}
+*/
+var uncompress = function(data){
+
+	var decoded = '0123456789abcdef'
+	, encoded = 'klmnopqrstuvwxyz'
+	, mapping = {}
+
+	for (var i = 0; i < encoded.length; i++){
+		mapping[encoded[i]] = decoded[i]
+	}
+
+	var undef
+	, output = {}
+	, sign = 1
+	, stringparts // undef. will be [] in string mode
+	
+	, activeobject = output
+	, parentchain = []
+	, parent_key_pair
+	, keyparts = ''
+	, valueparts = ''
+	, key // undef. will be Truthy when Key is resolved.
+	, datalen = data.length - 1 // stripping ending }
+	, ch
+
+	i = 1 // stripping starting {
+	
+	while (i != datalen){
+		// - { } ' are special.
+
+		ch = data[i]
+		i += 1
+
+		if (ch == "'"){
+			if (stringparts){
+				// end of string mode
+				key = stringparts.join('')
+				stringparts = undef				
+			} else {
+				// start of string mode
+				stringparts = []				
+			}
+		} else if (stringparts){
+			stringparts.push(ch)
+		} else if (ch == '{'){
+			// start of object
+			parentchain.push( [activeobject, key] )
+			activeobject = {}
+			key = undef
+		} else if (ch == '}'){
+			// end of object
+			parent_key_pair = parentchain.pop()
+			parent_key_pair[0][parent_key_pair[1]] = activeobject
+			key = undef
+			activeobject = parent_key_pair[0]
+		} else if (ch == '-'){
+			sign = -1
+		} else {
+			// must be number
+			if (key === undef) {
+				if (mapping.hasOwnProperty(ch)){
+					keyparts += mapping[ch]
+					key = parseInt(keyparts, 16) * sign
+					sign = +1
+					keyparts = ''
+				} else {
+					keyparts += ch
+				}
+			} else {
+				if (mapping.hasOwnProperty(ch)){
+					valueparts += mapping[ch]
+					activeobject[key] = parseInt(valueparts, 16) * sign
+					sign = +1
+					key = undef
+					valueparts = ''
+				} else {
+					valueparts += ch					
+				}
+			}
+		}
+	} // end while
+
+	return output
+}
+
+// encoding = 'Unicode' 
+// NOT UTF8, NOT UTF16BE/LE, NOT UCS2BE/LE. NO clever BOM behavior
+// Actual 16bit char codes used.
+// no multi-byte logic here
+
+// Unicode characters to WinAnsiEncoding:
+// {402: 131, 8211: 150, 8212: 151, 8216: 145, 8217: 146, 8218: 130, 8220: 147, 8221: 148, 8222: 132, 8224: 134, 8225: 135, 8226: 149, 8230: 133, 8364: 128, 8240:137, 8249: 139, 8250: 155, 710: 136, 8482: 153, 338: 140, 339: 156, 732: 152, 352: 138, 353: 154, 376: 159, 381: 142, 382: 158}
+// as you can see, all Unicode chars are outside of 0-255 range. No char code conflicts.
+// this means that you can give Win cp1252 encoded strings to jsPDF for rendering directly
+// as well as give strings with some (supported by these fonts) Unicode characters and 
+// these will be mapped to win cp1252 
+// for example, you can send char code (cp1252) 0x80 or (unicode) 0x20AC, getting "Euro" glyph displayed in both cases.
+
+var encodingBlock = {
+	'codePages': ['WinAnsiEncoding']
+	, 'WinAnsiEncoding': uncompress("{19m8n201n9q201o9r201s9l201t9m201u8m201w9n201x9o201y8o202k8q202l8r202m9p202q8p20aw8k203k8t203t8v203u9v2cq8s212m9t15m8w15n9w2dw9s16k8u16l9u17s9z17x8y17y9y}")
+}
+, encodings = {'Unicode':{
+	'Courier': encodingBlock
+	, 'Courier-Bold': encodingBlock
+	, 'Courier-BoldOblique': encodingBlock
+	, 'Courier-Oblique': encodingBlock
+	, 'Helvetica': encodingBlock
+	, 'Helvetica-Bold': encodingBlock
+	, 'Helvetica-BoldOblique': encodingBlock
+	, 'Helvetica-Oblique': encodingBlock
+	, 'Times-Roman': encodingBlock
+	, 'Times-Bold': encodingBlock
+	, 'Times-BoldItalic': encodingBlock
+	, 'Times-Italic': encodingBlock
+//	, 'Symbol'
+//	, 'ZapfDingbats'
+}}
+/** 
+Resources:
+Font metrics data is reprocessed derivative of contents of
+"Font Metrics for PDF Core 14 Fonts" package, which exhibits the following copyright and license:
+
+Copyright (c) 1989, 1990, 1991, 1992, 1993, 1997 Adobe Systems Incorporated. All Rights Reserved.
+
+This file and the 14 PostScript(R) AFM files it accompanies may be used,
+copied, and distributed for any purpose and without charge, with or without
+modification, provided that all copyright notices are retained; that the AFM
+files are not distributed without this file; that all modifications to this
+file or any of the AFM files are prominently noted in the modified file(s);
+and that this paragraph is not modified. Adobe Systems has no responsibility
+or obligation to support the use of the AFM files.
+
+*/
+, fontMetrics = {'Unicode':{
+	// all sizing numbers are n/fontMetricsFractionOf = one font size unit
+	// this means that if fontMetricsFractionOf = 1000, and letter A's width is 476, it's
+	// width is 476/1000 or 47.6% of its height (regardless of font size)
+	// At this time this value applies to "widths" and "kerning" numbers.
+
+	// char code 0 represents "default" (average) width - use it for chars missing in this table.
+	// key 'fof' represents the "fontMetricsFractionOf" value
+
+	'Courier-Oblique': uncompress("{'widths'{k3w'fof'6o}'kerning'{'fof'-6o}}")
+	, 'Times-BoldItalic': uncompress("{'widths'{k3o2q4ycx2r201n3m201o6o201s2l201t2l201u2l201w3m201x3m201y3m2k1t2l2r202m2n2n3m2o3m2p5n202q6o2r1w2s2l2t2l2u3m2v3t2w1t2x2l2y1t2z1w3k3m3l3m3m3m3n3m3o3m3p3m3q3m3r3m3s3m203t2l203u2l3v2l3w3t3x3t3y3t3z3m4k5n4l4m4m4m4n4m4o4s4p4m4q4m4r4s4s4y4t2r4u3m4v4m4w3x4x5t4y4s4z4s5k3x5l4s5m4m5n3r5o3x5p4s5q4m5r5t5s4m5t3x5u3x5v2l5w1w5x2l5y3t5z3m6k2l6l3m6m3m6n2w6o3m6p2w6q2l6r3m6s3r6t1w6u1w6v3m6w1w6x4y6y3r6z3m7k3m7l3m7m2r7n2r7o1w7p3r7q2w7r4m7s3m7t2w7u2r7v2n7w1q7x2n7y3t202l3mcl4mal2ram3man3mao3map3mar3mas2lat4uau1uav3maw3way4uaz2lbk2sbl3t'fof'6obo2lbp3tbq3mbr1tbs2lbu1ybv3mbz3mck4m202k3mcm4mcn4mco4mcp4mcq5ycr4mcs4mct4mcu4mcv4mcw2r2m3rcy2rcz2rdl4sdm4sdn4sdo4sdp4sdq4sds4sdt4sdu4sdv4sdw4sdz3mek3mel3mem3men3meo3mep3meq4ser2wes2wet2weu2wev2wew1wex1wey1wez1wfl3rfm3mfn3mfo3mfp3mfq3mfr3tfs3mft3rfu3rfv3rfw3rfz2w203k6o212m6o2dw2l2cq2l3t3m3u2l17s3x19m3m}'kerning'{cl{4qu5kt5qt5rs17ss5ts}201s{201ss}201t{cks4lscmscnscoscpscls2wu2yu201ts}201x{2wu2yu}2k{201ts}2w{4qx5kx5ou5qx5rs17su5tu}2x{17su5tu5ou}2y{4qx5kx5ou5qx5rs17ss5ts}'fof'-6ofn{17sw5tw5ou5qw5rs}7t{cksclscmscnscoscps4ls}3u{17su5tu5os5qs}3v{17su5tu5os5qs}7p{17su5tu}ck{4qu5kt5qt5rs17ss5ts}4l{4qu5kt5qt5rs17ss5ts}cm{4qu5kt5qt5rs17ss5ts}cn{4qu5kt5qt5rs17ss5ts}co{4qu5kt5qt5rs17ss5ts}cp{4qu5kt5qt5rs17ss5ts}6l{4qu5ou5qw5rt17su5tu}5q{ckuclucmucnucoucpu4lu}5r{ckuclucmucnucoucpu4lu}7q{cksclscmscnscoscps4ls}6p{4qu5ou5qw5rt17sw5tw}ek{4qu5ou5qw5rt17su5tu}el{4qu5ou5qw5rt17su5tu}em{4qu5ou5qw5rt17su5tu}en{4qu5ou5qw5rt17su5tu}eo{4qu5ou5qw5rt17su5tu}ep{4qu5ou5qw5rt17su5tu}es{17ss5ts5qs4qu}et{4qu5ou5qw5rt17sw5tw}eu{4qu5ou5qw5rt17ss5ts}ev{17ss5ts5qs4qu}6z{17sw5tw5ou5qw5rs}fm{17sw5tw5ou5qw5rs}7n{201ts}fo{17sw5tw5ou5qw5rs}fp{17sw5tw5ou5qw5rs}fq{17sw5tw5ou5qw5rs}7r{cksclscmscnscoscps4ls}fs{17sw5tw5ou5qw5rs}ft{17su5tu}fu{17su5tu}fv{17su5tu}fw{17su5tu}fz{cksclscmscnscoscps4ls}}}")
+	, 'Helvetica-Bold': uncompress("{'widths'{k3s2q4scx1w201n3r201o6o201s1w201t1w201u1w201w3m201x3m201y3m2k1w2l2l202m2n2n3r2o3r2p5t202q6o2r1s2s2l2t2l2u2r2v3u2w1w2x2l2y1w2z1w3k3r3l3r3m3r3n3r3o3r3p3r3q3r3r3r3s3r203t2l203u2l3v2l3w3u3x3u3y3u3z3x4k6l4l4s4m4s4n4s4o4s4p4m4q3x4r4y4s4s4t1w4u3r4v4s4w3x4x5n4y4s4z4y5k4m5l4y5m4s5n4m5o3x5p4s5q4m5r5y5s4m5t4m5u3x5v2l5w1w5x2l5y3u5z3r6k2l6l3r6m3x6n3r6o3x6p3r6q2l6r3x6s3x6t1w6u1w6v3r6w1w6x5t6y3x6z3x7k3x7l3x7m2r7n3r7o2l7p3x7q3r7r4y7s3r7t3r7u3m7v2r7w1w7x2r7y3u202l3rcl4sal2lam3ran3rao3rap3rar3ras2lat4tau2pav3raw3uay4taz2lbk2sbl3u'fof'6obo2lbp3xbq3rbr1wbs2lbu2obv3rbz3xck4s202k3rcm4scn4sco4scp4scq6ocr4scs4mct4mcu4mcv4mcw1w2m2zcy1wcz1wdl4sdm4ydn4ydo4ydp4ydq4yds4ydt4sdu4sdv4sdw4sdz3xek3rel3rem3ren3reo3rep3req5ter3res3ret3reu3rev3rew1wex1wey1wez1wfl3xfm3xfn3xfo3xfp3xfq3xfr3ufs3xft3xfu3xfv3xfw3xfz3r203k6o212m6o2dw2l2cq2l3t3r3u2l17s4m19m3r}'kerning'{cl{4qs5ku5ot5qs17sv5tv}201t{2ww4wy2yw}201w{2ks}201x{2ww4wy2yw}2k{201ts201xs}2w{7qs4qu5kw5os5qw5rs17su5tu7tsfzs}2x{5ow5qs}2y{7qs4qu5kw5os5qw5rs17su5tu7tsfzs}'fof'-6o7p{17su5tu5ot}ck{4qs5ku5ot5qs17sv5tv}4l{4qs5ku5ot5qs17sv5tv}cm{4qs5ku5ot5qs17sv5tv}cn{4qs5ku5ot5qs17sv5tv}co{4qs5ku5ot5qs17sv5tv}cp{4qs5ku5ot5qs17sv5tv}6l{17st5tt5os}17s{2kwclvcmvcnvcovcpv4lv4wwckv}5o{2kucltcmtcntcotcpt4lt4wtckt}5q{2ksclscmscnscoscps4ls4wvcks}5r{2ks4ws}5t{2kwclvcmvcnvcovcpv4lv4wwckv}eo{17st5tt5os}fu{17su5tu5ot}6p{17ss5ts}ek{17st5tt5os}el{17st5tt5os}em{17st5tt5os}en{17st5tt5os}6o{201ts}ep{17st5tt5os}es{17ss5ts}et{17ss5ts}eu{17ss5ts}ev{17ss5ts}6z{17su5tu5os5qt}fm{17su5tu5os5qt}fn{17su5tu5os5qt}fo{17su5tu5os5qt}fp{17su5tu5os5qt}fq{17su5tu5os5qt}fs{17su5tu5os5qt}ft{17su5tu5ot}7m{5os}fv{17su5tu5ot}fw{17su5tu5ot}}}")
+	, 'Courier': uncompress("{'widths'{k3w'fof'6o}'kerning'{'fof'-6o}}")
+	, 'Courier-BoldOblique': uncompress("{'widths'{k3w'fof'6o}'kerning'{'fof'-6o}}")
+	, 'Times-Bold': uncompress("{'widths'{k3q2q5ncx2r201n3m201o6o201s2l201t2l201u2l201w3m201x3m201y3m2k1t2l2l202m2n2n3m2o3m2p6o202q6o2r1w2s2l2t2l2u3m2v3t2w1t2x2l2y1t2z1w3k3m3l3m3m3m3n3m3o3m3p3m3q3m3r3m3s3m203t2l203u2l3v2l3w3t3x3t3y3t3z3m4k5x4l4s4m4m4n4s4o4s4p4m4q3x4r4y4s4y4t2r4u3m4v4y4w4m4x5y4y4s4z4y5k3x5l4y5m4s5n3r5o4m5p4s5q4s5r6o5s4s5t4s5u4m5v2l5w1w5x2l5y3u5z3m6k2l6l3m6m3r6n2w6o3r6p2w6q2l6r3m6s3r6t1w6u2l6v3r6w1w6x5n6y3r6z3m7k3r7l3r7m2w7n2r7o2l7p3r7q3m7r4s7s3m7t3m7u2w7v2r7w1q7x2r7y3o202l3mcl4sal2lam3man3mao3map3mar3mas2lat4uau1yav3maw3tay4uaz2lbk2sbl3t'fof'6obo2lbp3rbr1tbs2lbu2lbv3mbz3mck4s202k3mcm4scn4sco4scp4scq6ocr4scs4mct4mcu4mcv4mcw2r2m3rcy2rcz2rdl4sdm4ydn4ydo4ydp4ydq4yds4ydt4sdu4sdv4sdw4sdz3rek3mel3mem3men3meo3mep3meq4ser2wes2wet2weu2wev2wew1wex1wey1wez1wfl3rfm3mfn3mfo3mfp3mfq3mfr3tfs3mft3rfu3rfv3rfw3rfz3m203k6o212m6o2dw2l2cq2l3t3m3u2l17s4s19m3m}'kerning'{cl{4qt5ks5ot5qy5rw17sv5tv}201t{cks4lscmscnscoscpscls4wv}2k{201ts}2w{4qu5ku7mu5os5qx5ru17su5tu}2x{17su5tu5ou5qs}2y{4qv5kv7mu5ot5qz5ru17su5tu}'fof'-6o7t{cksclscmscnscoscps4ls}3u{17su5tu5os5qu}3v{17su5tu5os5qu}fu{17su5tu5ou5qu}7p{17su5tu5ou5qu}ck{4qt5ks5ot5qy5rw17sv5tv}4l{4qt5ks5ot5qy5rw17sv5tv}cm{4qt5ks5ot5qy5rw17sv5tv}cn{4qt5ks5ot5qy5rw17sv5tv}co{4qt5ks5ot5qy5rw17sv5tv}cp{4qt5ks5ot5qy5rw17sv5tv}6l{17st5tt5ou5qu}17s{ckuclucmucnucoucpu4lu4wu}5o{ckuclucmucnucoucpu4lu4wu}5q{ckzclzcmzcnzcozcpz4lz4wu}5r{ckxclxcmxcnxcoxcpx4lx4wu}5t{ckuclucmucnucoucpu4lu4wu}7q{ckuclucmucnucoucpu4lu}6p{17sw5tw5ou5qu}ek{17st5tt5qu}el{17st5tt5ou5qu}em{17st5tt5qu}en{17st5tt5qu}eo{17st5tt5qu}ep{17st5tt5ou5qu}es{17ss5ts5qu}et{17sw5tw5ou5qu}eu{17sw5tw5ou5qu}ev{17ss5ts5qu}6z{17sw5tw5ou5qu5rs}fm{17sw5tw5ou5qu5rs}fn{17sw5tw5ou5qu5rs}fo{17sw5tw5ou5qu5rs}fp{17sw5tw5ou5qu5rs}fq{17sw5tw5ou5qu5rs}7r{cktcltcmtcntcotcpt4lt5os}fs{17sw5tw5ou5qu5rs}ft{17su5tu5ou5qu}7m{5os}fv{17su5tu5ou5qu}fw{17su5tu5ou5qu}fz{cksclscmscnscoscps4ls}}}")
+	//, 'Symbol': uncompress("{'widths'{k3uaw4r19m3m2k1t2l2l202m2y2n3m2p5n202q6o3k3m2s2l2t2l2v3r2w1t3m3m2y1t2z1wbk2sbl3r'fof'6o3n3m3o3m3p3m3q3m3r3m3s3m3t3m3u1w3v1w3w3r3x3r3y3r3z2wbp3t3l3m5v2l5x2l5z3m2q4yfr3r7v3k7w1o7x3k}'kerning'{'fof'-6o}}")
+	, 'Helvetica': uncompress("{'widths'{k3p2q4mcx1w201n3r201o6o201s1q201t1q201u1q201w2l201x2l201y2l2k1w2l1w202m2n2n3r2o3r2p5t202q6o2r1n2s2l2t2l2u2r2v3u2w1w2x2l2y1w2z1w3k3r3l3r3m3r3n3r3o3r3p3r3q3r3r3r3s3r203t2l203u2l3v1w3w3u3x3u3y3u3z3r4k6p4l4m4m4m4n4s4o4s4p4m4q3x4r4y4s4s4t1w4u3m4v4m4w3r4x5n4y4s4z4y5k4m5l4y5m4s5n4m5o3x5p4s5q4m5r5y5s4m5t4m5u3x5v1w5w1w5x1w5y2z5z3r6k2l6l3r6m3r6n3m6o3r6p3r6q1w6r3r6s3r6t1q6u1q6v3m6w1q6x5n6y3r6z3r7k3r7l3r7m2l7n3m7o1w7p3r7q3m7r4s7s3m7t3m7u3m7v2l7w1u7x2l7y3u202l3rcl4mal2lam3ran3rao3rap3rar3ras2lat4tau2pav3raw3uay4taz2lbk2sbl3u'fof'6obo2lbp3rbr1wbs2lbu2obv3rbz3xck4m202k3rcm4mcn4mco4mcp4mcq6ocr4scs4mct4mcu4mcv4mcw1w2m2ncy1wcz1wdl4sdm4ydn4ydo4ydp4ydq4yds4ydt4sdu4sdv4sdw4sdz3xek3rel3rem3ren3reo3rep3req5ter3mes3ret3reu3rev3rew1wex1wey1wez1wfl3rfm3rfn3rfo3rfp3rfq3rfr3ufs3xft3rfu3rfv3rfw3rfz3m203k6o212m6o2dw2l2cq2l3t3r3u1w17s4m19m3r}'kerning'{5q{4wv}cl{4qs5kw5ow5qs17sv5tv}201t{2wu4w1k2yu}201x{2wu4wy2yu}17s{2ktclucmucnu4otcpu4lu4wycoucku}2w{7qs4qz5k1m17sy5ow5qx5rsfsu5ty7tufzu}2x{17sy5ty5oy5qs}2y{7qs4qz5k1m17sy5ow5qx5rsfsu5ty7tufzu}'fof'-6o7p{17sv5tv5ow}ck{4qs5kw5ow5qs17sv5tv}4l{4qs5kw5ow5qs17sv5tv}cm{4qs5kw5ow5qs17sv5tv}cn{4qs5kw5ow5qs17sv5tv}co{4qs5kw5ow5qs17sv5tv}cp{4qs5kw5ow5qs17sv5tv}6l{17sy5ty5ow}do{17st5tt}4z{17st5tt}7s{fst}dm{17st5tt}dn{17st5tt}5o{ckwclwcmwcnwcowcpw4lw4wv}dp{17st5tt}dq{17st5tt}7t{5ow}ds{17st5tt}5t{2ktclucmucnu4otcpu4lu4wycoucku}fu{17sv5tv5ow}6p{17sy5ty5ow5qs}ek{17sy5ty5ow}el{17sy5ty5ow}em{17sy5ty5ow}en{5ty}eo{17sy5ty5ow}ep{17sy5ty5ow}es{17sy5ty5qs}et{17sy5ty5ow5qs}eu{17sy5ty5ow5qs}ev{17sy5ty5ow5qs}6z{17sy5ty5ow5qs}fm{17sy5ty5ow5qs}fn{17sy5ty5ow5qs}fo{17sy5ty5ow5qs}fp{17sy5ty5qs}fq{17sy5ty5ow5qs}7r{5ow}fs{17sy5ty5ow5qs}ft{17sv5tv5ow}7m{5ow}fv{17sv5tv5ow}fw{17sv5tv5ow}}}")
+	, 'Helvetica-BoldOblique': uncompress("{'widths'{k3s2q4scx1w201n3r201o6o201s1w201t1w201u1w201w3m201x3m201y3m2k1w2l2l202m2n2n3r2o3r2p5t202q6o2r1s2s2l2t2l2u2r2v3u2w1w2x2l2y1w2z1w3k3r3l3r3m3r3n3r3o3r3p3r3q3r3r3r3s3r203t2l203u2l3v2l3w3u3x3u3y3u3z3x4k6l4l4s4m4s4n4s4o4s4p4m4q3x4r4y4s4s4t1w4u3r4v4s4w3x4x5n4y4s4z4y5k4m5l4y5m4s5n4m5o3x5p4s5q4m5r5y5s4m5t4m5u3x5v2l5w1w5x2l5y3u5z3r6k2l6l3r6m3x6n3r6o3x6p3r6q2l6r3x6s3x6t1w6u1w6v3r6w1w6x5t6y3x6z3x7k3x7l3x7m2r7n3r7o2l7p3x7q3r7r4y7s3r7t3r7u3m7v2r7w1w7x2r7y3u202l3rcl4sal2lam3ran3rao3rap3rar3ras2lat4tau2pav3raw3uay4taz2lbk2sbl3u'fof'6obo2lbp3xbq3rbr1wbs2lbu2obv3rbz3xck4s202k3rcm4scn4sco4scp4scq6ocr4scs4mct4mcu4mcv4mcw1w2m2zcy1wcz1wdl4sdm4ydn4ydo4ydp4ydq4yds4ydt4sdu4sdv4sdw4sdz3xek3rel3rem3ren3reo3rep3req5ter3res3ret3reu3rev3rew1wex1wey1wez1wfl3xfm3xfn3xfo3xfp3xfq3xfr3ufs3xft3xfu3xfv3xfw3xfz3r203k6o212m6o2dw2l2cq2l3t3r3u2l17s4m19m3r}'kerning'{cl{4qs5ku5ot5qs17sv5tv}201t{2ww4wy2yw}201w{2ks}201x{2ww4wy2yw}2k{201ts201xs}2w{7qs4qu5kw5os5qw5rs17su5tu7tsfzs}2x{5ow5qs}2y{7qs4qu5kw5os5qw5rs17su5tu7tsfzs}'fof'-6o7p{17su5tu5ot}ck{4qs5ku5ot5qs17sv5tv}4l{4qs5ku5ot5qs17sv5tv}cm{4qs5ku5ot5qs17sv5tv}cn{4qs5ku5ot5qs17sv5tv}co{4qs5ku5ot5qs17sv5tv}cp{4qs5ku5ot5qs17sv5tv}6l{17st5tt5os}17s{2kwclvcmvcnvcovcpv4lv4wwckv}5o{2kucltcmtcntcotcpt4lt4wtckt}5q{2ksclscmscnscoscps4ls4wvcks}5r{2ks4ws}5t{2kwclvcmvcnvcovcpv4lv4wwckv}eo{17st5tt5os}fu{17su5tu5ot}6p{17ss5ts}ek{17st5tt5os}el{17st5tt5os}em{17st5tt5os}en{17st5tt5os}6o{201ts}ep{17st5tt5os}es{17ss5ts}et{17ss5ts}eu{17ss5ts}ev{17ss5ts}6z{17su5tu5os5qt}fm{17su5tu5os5qt}fn{17su5tu5os5qt}fo{17su5tu5os5qt}fp{17su5tu5os5qt}fq{17su5tu5os5qt}fs{17su5tu5os5qt}ft{17su5tu5ot}7m{5os}fv{17su5tu5ot}fw{17su5tu5ot}}}")
+	//, 'ZapfDingbats': uncompress("{'widths'{k4u2k1w'fof'6o}'kerning'{'fof'-6o}}")
+	, 'Courier-Bold': uncompress("{'widths'{k3w'fof'6o}'kerning'{'fof'-6o}}")
+	, 'Times-Italic': uncompress("{'widths'{k3n2q4ycx2l201n3m201o5t201s2l201t2l201u2l201w3r201x3r201y3r2k1t2l2l202m2n2n3m2o3m2p5n202q5t2r1p2s2l2t2l2u3m2v4n2w1t2x2l2y1t2z1w3k3m3l3m3m3m3n3m3o3m3p3m3q3m3r3m3s3m203t2l203u2l3v2l3w4n3x4n3y4n3z3m4k5w4l3x4m3x4n4m4o4s4p3x4q3x4r4s4s4s4t2l4u2w4v4m4w3r4x5n4y4m4z4s5k3x5l4s5m3x5n3m5o3r5p4s5q3x5r5n5s3x5t3r5u3r5v2r5w1w5x2r5y2u5z3m6k2l6l3m6m3m6n2w6o3m6p2w6q1w6r3m6s3m6t1w6u1w6v2w6w1w6x4s6y3m6z3m7k3m7l3m7m2r7n2r7o1w7p3m7q2w7r4m7s2w7t2w7u2r7v2s7w1v7x2s7y3q202l3mcl3xal2ram3man3mao3map3mar3mas2lat4wau1vav3maw4nay4waz2lbk2sbl4n'fof'6obo2lbp3mbq3obr1tbs2lbu1zbv3mbz3mck3x202k3mcm3xcn3xco3xcp3xcq5tcr4mcs3xct3xcu3xcv3xcw2l2m2ucy2lcz2ldl4mdm4sdn4sdo4sdp4sdq4sds4sdt4sdu4sdv4sdw4sdz3mek3mel3mem3men3meo3mep3meq4mer2wes2wet2weu2wev2wew1wex1wey1wez1wfl3mfm3mfn3mfo3mfp3mfq3mfr4nfs3mft3mfu3mfv3mfw3mfz2w203k6o212m6m2dw2l2cq2l3t3m3u2l17s3r19m3m}'kerning'{cl{5kt4qw}201s{201sw}201t{201tw2wy2yy6q-t}201x{2wy2yy}2k{201tw}2w{7qs4qy7rs5ky7mw5os5qx5ru17su5tu}2x{17ss5ts5os}2y{7qs4qy7rs5ky7mw5os5qx5ru17su5tu}'fof'-6o6t{17ss5ts5qs}7t{5os}3v{5qs}7p{17su5tu5qs}ck{5kt4qw}4l{5kt4qw}cm{5kt4qw}cn{5kt4qw}co{5kt4qw}cp{5kt4qw}6l{4qs5ks5ou5qw5ru17su5tu}17s{2ks}5q{ckvclvcmvcnvcovcpv4lv}5r{ckuclucmucnucoucpu4lu}5t{2ks}6p{4qs5ks5ou5qw5ru17su5tu}ek{4qs5ks5ou5qw5ru17su5tu}el{4qs5ks5ou5qw5ru17su5tu}em{4qs5ks5ou5qw5ru17su5tu}en{4qs5ks5ou5qw5ru17su5tu}eo{4qs5ks5ou5qw5ru17su5tu}ep{4qs5ks5ou5qw5ru17su5tu}es{5ks5qs4qs}et{4qs5ks5ou5qw5ru17su5tu}eu{4qs5ks5qw5ru17su5tu}ev{5ks5qs4qs}ex{17ss5ts5qs}6z{4qv5ks5ou5qw5ru17su5tu}fm{4qv5ks5ou5qw5ru17su5tu}fn{4qv5ks5ou5qw5ru17su5tu}fo{4qv5ks5ou5qw5ru17su5tu}fp{4qv5ks5ou5qw5ru17su5tu}fq{4qv5ks5ou5qw5ru17su5tu}7r{5os}fs{4qv5ks5ou5qw5ru17su5tu}ft{17su5tu5qs}fu{17su5tu5qs}fv{17su5tu5qs}fw{17su5tu5qs}}}")
+	, 'Times-Roman': uncompress("{'widths'{k3n2q4ycx2l201n3m201o6o201s2l201t2l201u2l201w2w201x2w201y2w2k1t2l2l202m2n2n3m2o3m2p5n202q6o2r1m2s2l2t2l2u3m2v3s2w1t2x2l2y1t2z1w3k3m3l3m3m3m3n3m3o3m3p3m3q3m3r3m3s3m203t2l203u2l3v1w3w3s3x3s3y3s3z2w4k5w4l4s4m4m4n4m4o4s4p3x4q3r4r4s4s4s4t2l4u2r4v4s4w3x4x5t4y4s4z4s5k3r5l4s5m4m5n3r5o3x5p4s5q4s5r5y5s4s5t4s5u3x5v2l5w1w5x2l5y2z5z3m6k2l6l2w6m3m6n2w6o3m6p2w6q2l6r3m6s3m6t1w6u1w6v3m6w1w6x4y6y3m6z3m7k3m7l3m7m2l7n2r7o1w7p3m7q3m7r4s7s3m7t3m7u2w7v3k7w1o7x3k7y3q202l3mcl4sal2lam3man3mao3map3mar3mas2lat4wau1vav3maw3say4waz2lbk2sbl3s'fof'6obo2lbp3mbq2xbr1tbs2lbu1zbv3mbz2wck4s202k3mcm4scn4sco4scp4scq5tcr4mcs3xct3xcu3xcv3xcw2l2m2tcy2lcz2ldl4sdm4sdn4sdo4sdp4sdq4sds4sdt4sdu4sdv4sdw4sdz3mek2wel2wem2wen2weo2wep2weq4mer2wes2wet2weu2wev2wew1wex1wey1wez1wfl3mfm3mfn3mfo3mfp3mfq3mfr3sfs3mft3mfu3mfv3mfw3mfz3m203k6o212m6m2dw2l2cq2l3t3m3u1w17s4s19m3m}'kerning'{cl{4qs5ku17sw5ou5qy5rw201ss5tw201ws}201s{201ss}201t{ckw4lwcmwcnwcowcpwclw4wu201ts}2k{201ts}2w{4qs5kw5os5qx5ru17sx5tx}2x{17sw5tw5ou5qu}2y{4qs5kw5os5qx5ru17sx5tx}'fof'-6o7t{ckuclucmucnucoucpu4lu5os5rs}3u{17su5tu5qs}3v{17su5tu5qs}7p{17sw5tw5qs}ck{4qs5ku17sw5ou5qy5rw201ss5tw201ws}4l{4qs5ku17sw5ou5qy5rw201ss5tw201ws}cm{4qs5ku17sw5ou5qy5rw201ss5tw201ws}cn{4qs5ku17sw5ou5qy5rw201ss5tw201ws}co{4qs5ku17sw5ou5qy5rw201ss5tw201ws}cp{4qs5ku17sw5ou5qy5rw201ss5tw201ws}6l{17su5tu5os5qw5rs}17s{2ktclvcmvcnvcovcpv4lv4wuckv}5o{ckwclwcmwcnwcowcpw4lw4wu}5q{ckyclycmycnycoycpy4ly4wu5ms}5r{cktcltcmtcntcotcpt4lt4ws}5t{2ktclvcmvcnvcovcpv4lv4wuckv}7q{cksclscmscnscoscps4ls}6p{17su5tu5qw5rs}ek{5qs5rs}el{17su5tu5os5qw5rs}em{17su5tu5os5qs5rs}en{17su5qs5rs}eo{5qs5rs}ep{17su5tu5os5qw5rs}es{5qs}et{17su5tu5qw5rs}eu{17su5tu5qs5rs}ev{5qs}6z{17sv5tv5os5qx5rs}fm{5os5qt5rs}fn{17sv5tv5os5qx5rs}fo{17sv5tv5os5qx5rs}fp{5os5qt5rs}fq{5os5qt5rs}7r{ckuclucmucnucoucpu4lu5os}fs{17sv5tv5os5qx5rs}ft{17ss5ts5qs}fu{17sw5tw5qs}fv{17sw5tw5qs}fw{17ss5ts5qs}fz{ckuclucmucnucoucpu4lu5os5rs}}}")
+	, 'Helvetica-Oblique': uncompress("{'widths'{k3p2q4mcx1w201n3r201o6o201s1q201t1q201u1q201w2l201x2l201y2l2k1w2l1w202m2n2n3r2o3r2p5t202q6o2r1n2s2l2t2l2u2r2v3u2w1w2x2l2y1w2z1w3k3r3l3r3m3r3n3r3o3r3p3r3q3r3r3r3s3r203t2l203u2l3v1w3w3u3x3u3y3u3z3r4k6p4l4m4m4m4n4s4o4s4p4m4q3x4r4y4s4s4t1w4u3m4v4m4w3r4x5n4y4s4z4y5k4m5l4y5m4s5n4m5o3x5p4s5q4m5r5y5s4m5t4m5u3x5v1w5w1w5x1w5y2z5z3r6k2l6l3r6m3r6n3m6o3r6p3r6q1w6r3r6s3r6t1q6u1q6v3m6w1q6x5n6y3r6z3r7k3r7l3r7m2l7n3m7o1w7p3r7q3m7r4s7s3m7t3m7u3m7v2l7w1u7x2l7y3u202l3rcl4mal2lam3ran3rao3rap3rar3ras2lat4tau2pav3raw3uay4taz2lbk2sbl3u'fof'6obo2lbp3rbr1wbs2lbu2obv3rbz3xck4m202k3rcm4mcn4mco4mcp4mcq6ocr4scs4mct4mcu4mcv4mcw1w2m2ncy1wcz1wdl4sdm4ydn4ydo4ydp4ydq4yds4ydt4sdu4sdv4sdw4sdz3xek3rel3rem3ren3reo3rep3req5ter3mes3ret3reu3rev3rew1wex1wey1wez1wfl3rfm3rfn3rfo3rfp3rfq3rfr3ufs3xft3rfu3rfv3rfw3rfz3m203k6o212m6o2dw2l2cq2l3t3r3u1w17s4m19m3r}'kerning'{5q{4wv}cl{4qs5kw5ow5qs17sv5tv}201t{2wu4w1k2yu}201x{2wu4wy2yu}17s{2ktclucmucnu4otcpu4lu4wycoucku}2w{7qs4qz5k1m17sy5ow5qx5rsfsu5ty7tufzu}2x{17sy5ty5oy5qs}2y{7qs4qz5k1m17sy5ow5qx5rsfsu5ty7tufzu}'fof'-6o7p{17sv5tv5ow}ck{4qs5kw5ow5qs17sv5tv}4l{4qs5kw5ow5qs17sv5tv}cm{4qs5kw5ow5qs17sv5tv}cn{4qs5kw5ow5qs17sv5tv}co{4qs5kw5ow5qs17sv5tv}cp{4qs5kw5ow5qs17sv5tv}6l{17sy5ty5ow}do{17st5tt}4z{17st5tt}7s{fst}dm{17st5tt}dn{17st5tt}5o{ckwclwcmwcnwcowcpw4lw4wv}dp{17st5tt}dq{17st5tt}7t{5ow}ds{17st5tt}5t{2ktclucmucnu4otcpu4lu4wycoucku}fu{17sv5tv5ow}6p{17sy5ty5ow5qs}ek{17sy5ty5ow}el{17sy5ty5ow}em{17sy5ty5ow}en{5ty}eo{17sy5ty5ow}ep{17sy5ty5ow}es{17sy5ty5qs}et{17sy5ty5ow5qs}eu{17sy5ty5ow5qs}ev{17sy5ty5ow5qs}6z{17sy5ty5ow5qs}fm{17sy5ty5ow5qs}fn{17sy5ty5ow5qs}fo{17sy5ty5ow5qs}fp{17sy5ty5qs}fq{17sy5ty5ow5qs}7r{5ow}fs{17sy5ty5ow5qs}ft{17sv5tv5ow}7m{5ow}fv{17sv5tv5ow}fw{17sv5tv5ow}}}")
+}};
+
+/*
+This event handler is fired when a new jsPDF object is initialized
+This event handler appends metrics data to standard fonts within
+that jsPDF instance. The metrics are mapped over Unicode character
+codes, NOT CIDs or other codes matching the StandardEncoding table of the
+standard PDF fonts.
+Future:
+Also included is the encoding maping table, converting Unicode (UCS-2, UTF-16)
+char codes to StandardEncoding character codes. The encoding table is to be used
+somewhere around "pdfEscape" call.
+*/
+
+API.events.push([ 
+	'addFonts'
+	,function(fontManagementObjects) {
+		// fontManagementObjects is {
+		//	'fonts':font_ID-keyed hash of font objects
+		//	, 'dictionary': lookup object, linking ["FontFamily"]['Style'] to font ID
+		//}
+		var font
+		, fontID
+		, metrics
+		, unicode_section
+		, encoding = 'Unicode'
+		, encodingBlock
+
+		for (fontID in fontManagementObjects.fonts){
+			if (fontManagementObjects.fonts.hasOwnProperty(fontID)) {
+				font = fontManagementObjects.fonts[fontID]
+
+				// // we only ship 'Unicode' mappings and metrics. No need for loop.
+				// // still, leaving this for the future.
+
+				// for (encoding in fontMetrics){
+				// 	if (fontMetrics.hasOwnProperty(encoding)) {
+
+						metrics = fontMetrics[encoding][font.PostScriptName]
+						if (metrics) {
+							if (font.metadata[encoding]) {
+								unicode_section = font.metadata[encoding]
+							} else {
+								unicode_section = font.metadata[encoding] = {}
+							}
+
+							unicode_section.widths = metrics.widths
+							unicode_section.kerning = metrics.kerning
+						}
+				// 	}
+				// }
+				// for (encoding in encodings){
+				// 	if (encodings.hasOwnProperty(encoding)) {
+						encodingBlock = encodings[encoding][font.PostScriptName]
+						if (encodingBlock) {
+							if (font.metadata[encoding]) {
+								unicode_section = font.metadata[encoding]
+							} else {
+								unicode_section = font.metadata[encoding] = {}
+							}
+
+							unicode_section.encoding = encodingBlock
+							if (encodingBlock.codePages && encodingBlock.codePages.length) {
+								font.encoding = encodingBlock.codePages[0]
+							}
+						}
+				// 	}
+				// }
+			}
+		}
+	}
+]) // end of adding event handler
+
+})(jsPDF.API);
+/** ==================================================================== 
+ * jsPDF total_pages plugin
+ * Copyright (c) 2013 Eduardo Menezes de Morais, eduardo.morais@usp.br
+ * 
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ * ====================================================================
+ */
+
+(function(jsPDFAPI) {
+'use strict';
+
+jsPDFAPI.putTotalPages = function(pageExpression) {
+	'use strict';
+        var replaceExpression = new RegExp(pageExpression, 'g');
+        for (var n = 1; n <= this.internal.getNumberOfPages(); n++) {
+            for (var i = 0; i < this.internal.pages[n].length; i++)
+               this.internal.pages[n][i] = this.internal.pages[n][i].replace(replaceExpression, this.internal.getNumberOfPages());
+        }
+	return this;
+};
+
+})(jsPDF.API);
+/* Blob.js
+ * A Blob implementation.
+ * 2014-07-24
+ *
+ * By Eli Grey, http://eligrey.com
+ * By Devin Samarin, https://github.com/dsamarin
+ * License: X11/MIT
+ *   See https://github.com/eligrey/Blob.js/blob/master/LICENSE.md
+ */
+
+/*global self, unescape */
+/*jslint bitwise: true, regexp: true, confusion: true, es5: true, vars: true, white: true,
+  plusplus: true */
+
+/*! @source http://purl.eligrey.com/github/Blob.js/blob/master/Blob.js */
+
+(function (view) {
+	"use strict";
+
+	view.URL = view.URL || view.webkitURL;
+
+	if (view.Blob && view.URL) {
+		try {
+			new Blob;
+			return;
+		} catch (e) {}
+	}
+
+	// Internally we use a BlobBuilder implementation to base Blob off of
+	// in order to support older browsers that only have BlobBuilder
+	var BlobBuilder = view.BlobBuilder || view.WebKitBlobBuilder || view.MozBlobBuilder || (function(view) {
+		var
+			  get_class = function(object) {
+				return Object.prototype.toString.call(object).match(/^\[object\s(.*)\]$/)[1];
+			}
+			, FakeBlobBuilder = function BlobBuilder() {
+				this.data = [];
+			}
+			, FakeBlob = function Blob(data, type, encoding) {
+				this.data = data;
+				this.size = data.length;
+				this.type = type;
+				this.encoding = encoding;
+			}
+			, FBB_proto = FakeBlobBuilder.prototype
+			, FB_proto = FakeBlob.prototype
+			, FileReaderSync = view.FileReaderSync
+			, FileException = function(type) {
+				this.code = this[this.name = type];
+			}
+			, file_ex_codes = (
+				  "NOT_FOUND_ERR SECURITY_ERR ABORT_ERR NOT_READABLE_ERR ENCODING_ERR "
+				+ "NO_MODIFICATION_ALLOWED_ERR INVALID_STATE_ERR SYNTAX_ERR"
+			).split(" ")
+			, file_ex_code = file_ex_codes.length
+			, real_URL = view.URL || view.webkitURL || view
+			, real_create_object_URL = real_URL.createObjectURL
+			, real_revoke_object_URL = real_URL.revokeObjectURL
+			, URL = real_URL
+			, btoa = view.btoa
+			, atob = view.atob
+
+			, ArrayBuffer = view.ArrayBuffer
+			, Uint8Array = view.Uint8Array
+
+			, origin = /^[\w-]+:\/*\[?[\w\.:-]+\]?(?::[0-9]+)?/
+		;
+		FakeBlob.fake = FB_proto.fake = true;
+		while (file_ex_code--) {
+			FileException.prototype[file_ex_codes[file_ex_code]] = file_ex_code + 1;
+		}
+		// Polyfill URL
+		if (!real_URL.createObjectURL) {
+			URL = view.URL = function(uri) {
+				var
+					  uri_info = document.createElementNS("http://www.w3.org/1999/xhtml", "a")
+					, uri_origin
+				;
+				uri_info.href = uri;
+				if (!("origin" in uri_info)) {
+					if (uri_info.protocol.toLowerCase() === "data:") {
+						uri_info.origin = null;
+					} else {
+						uri_origin = uri.match(origin);
+						uri_info.origin = uri_origin && uri_origin[1];
+					}
+				}
+				return uri_info;
+			};
+		}
+		URL.createObjectURL = function(blob) {
+			var
+				  type = blob.type
+				, data_URI_header
+			;
+			if (type === null) {
+				type = "application/octet-stream";
+			}
+			if (blob instanceof FakeBlob) {
+				data_URI_header = "data:" + type;
+				if (blob.encoding === "base64") {
+					return data_URI_header + ";base64," + blob.data;
+				} else if (blob.encoding === "URI") {
+					return data_URI_header + "," + decodeURIComponent(blob.data);
+				} if (btoa) {
+					return data_URI_header + ";base64," + btoa(blob.data);
+				} else {
+					return data_URI_header + "," + encodeURIComponent(blob.data);
+				}
+			} else if (real_create_object_URL) {
+				return real_create_object_URL.call(real_URL, blob);
+			}
+		};
+		URL.revokeObjectURL = function(object_URL) {
+			if (object_URL.substring(0, 5) !== "data:" && real_revoke_object_URL) {
+				real_revoke_object_URL.call(real_URL, object_URL);
+			}
+		};
+		FBB_proto.append = function(data/*, endings*/) {
+			var bb = this.data;
+			// decode data to a binary string
+			if (Uint8Array && (data instanceof ArrayBuffer || data instanceof Uint8Array)) {
+				var
+					  str = ""
+					, buf = new Uint8Array(data)
+					, i = 0
+					, buf_len = buf.length
+				;
+				for (; i < buf_len; i++) {
+					str += String.fromCharCode(buf[i]);
+				}
+				bb.push(str);
+			} else if (get_class(data) === "Blob" || get_class(data) === "File") {
+				if (FileReaderSync) {
+					var fr = new FileReaderSync;
+					bb.push(fr.readAsBinaryString(data));
+				} else {
+					// async FileReader won't work as BlobBuilder is sync
+					throw new FileException("NOT_READABLE_ERR");
+				}
+			} else if (data instanceof FakeBlob) {
+				if (data.encoding === "base64" && atob) {
+					bb.push(atob(data.data));
+				} else if (data.encoding === "URI") {
+					bb.push(decodeURIComponent(data.data));
+				} else if (data.encoding === "raw") {
+					bb.push(data.data);
+				}
+			} else {
+				if (typeof data !== "string") {
+					data += ""; // convert unsupported types to strings
+				}
+				// decode UTF-16 to binary string
+				bb.push(unescape(encodeURIComponent(data)));
+			}
+		};
+		FBB_proto.getBlob = function(type) {
+			if (!arguments.length) {
+				type = null;
+			}
+			return new FakeBlob(this.data.join(""), type, "raw");
+		};
+		FBB_proto.toString = function() {
+			return "[object BlobBuilder]";
+		};
+		FB_proto.slice = function(start, end, type) {
+			var args = arguments.length;
+			if (args < 3) {
+				type = null;
+			}
+			return new FakeBlob(
+				  this.data.slice(start, args > 1 ? end : this.data.length)
+				, type
+				, this.encoding
+			);
+		};
+		FB_proto.toString = function() {
+			return "[object Blob]";
+		};
+		FB_proto.close = function() {
+			this.size = 0;
+			delete this.data;
+		};
+		return FakeBlobBuilder;
+	}(view));
+
+	view.Blob = function(blobParts, options) {
+		var type = options ? (options.type || "") : "";
+		var builder = new BlobBuilder();
+		if (blobParts) {
+			for (var i = 0, len = blobParts.length; i < len; i++) {
+				builder.append(blobParts[i]);
+			}
+		}
+		return builder.getBlob(type);
+	};
+}(typeof self !== "undefined" && self || typeof window !== "undefined" && window || this.content || this));
+/* FileSaver.js
+ * A saveAs() FileSaver implementation.
+ * 2014-08-29
+ *
+ * By Eli Grey, http://eligrey.com
+ * License: X11/MIT
+ *   See https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md
+ */
+
+/*global self */
+/*jslint bitwise: true, indent: 4, laxbreak: true, laxcomma: true, smarttabs: true, plusplus: true */
+
+/*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */
+
+var saveAs = saveAs
+  // IE 10+ (native saveAs)
+  || (typeof navigator !== "undefined" &&
+      navigator.msSaveOrOpenBlob && navigator.msSaveOrOpenBlob.bind(navigator))
+  // Everyone else
+  || (function(view) {
+	"use strict";
+	// IE <10 is explicitly unsupported
+	if (typeof navigator !== "undefined" &&
+	    /MSIE [1-9]\./.test(navigator.userAgent)) {
+		return;
+	}
+	var
+		  doc = view.document
+		  // only get URL when necessary in case Blob.js hasn't overridden it yet
+		, get_URL = function() {
+			return view.URL || view.webkitURL || view;
+		}
+		, save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a")
+		, can_use_save_link = "download" in save_link
+		, click = function(node) {
+			var event = doc.createEvent("MouseEvents");
+			event.initMouseEvent(
+				"click", true, false, view, 0, 0, 0, 0, 0
+				, false, false, false, false, 0, null
+			);
+			node.dispatchEvent(event);
+		}
+		, webkit_req_fs = view.webkitRequestFileSystem
+		, req_fs = view.requestFileSystem || webkit_req_fs || view.mozRequestFileSystem
+		, throw_outside = function(ex) {
+			(view.setImmediate || view.setTimeout)(function() {
+				throw ex;
+			}, 0);
+		}
+		, force_saveable_type = "application/octet-stream"
+		, fs_min_size = 0
+		// See https://code.google.com/p/chromium/issues/detail?id=375297#c7 for
+		// the reasoning behind the timeout and revocation flow
+		, arbitrary_revoke_timeout = 10
+		, revoke = function(file) {
+			var revoker = function() {
+				if (typeof file === "string") { // file is an object URL
+					get_URL().revokeObjectURL(file);
+				} else { // file is a File
+					file.remove();
+				}
+			};
+			if (view.chrome) {
+				revoker();
+			} else {
+				setTimeout(revoker, arbitrary_revoke_timeout);
+			}
+		}
+		, dispatch = function(filesaver, event_types, event) {
+			event_types = [].concat(event_types);
+			var i = event_types.length;
+			while (i--) {
+				var listener = filesaver["on" + event_types[i]];
+				if (typeof listener === "function") {
+					try {
+						listener.call(filesaver, event || filesaver);
+					} catch (ex) {
+						throw_outside(ex);
+					}
+				}
+			}
+		}
+		, FileSaver = function(blob, name) {
+			// First try a.download, then web filesystem, then object URLs
+			var
+				  filesaver = this
+				, type = blob.type
+				, blob_changed = false
+				, object_url
+				, target_view
+				, dispatch_all = function() {
+					dispatch(filesaver, "writestart progress write writeend".split(" "));
+				}
+				// on any filesys errors revert to saving with object URLs
+				, fs_error = function() {
+					// don't create more object URLs than needed
+					if (blob_changed || !object_url) {
+						object_url = get_URL().createObjectURL(blob);
+					}
+					if (target_view) {
+						target_view.location.href = object_url;
+					} else {
+						var new_tab = view.open(object_url, "_blank");
+						if (new_tab == undefined && typeof safari !== "undefined") {
+							//Apple do not allow window.open, see http://bit.ly/1kZffRI
+							view.location.href = object_url
+						}
+					}
+					filesaver.readyState = filesaver.DONE;
+					dispatch_all();
+					revoke(object_url);
+				}
+				, abortable = function(func) {
+					return function() {
+						if (filesaver.readyState !== filesaver.DONE) {
+							return func.apply(this, arguments);
+						}
+					};
+				}
+				, create_if_not_found = {create: true, exclusive: false}
+				, slice
+			;
+			filesaver.readyState = filesaver.INIT;
+			if (!name) {
+				name = "download";
+			}
+			if (can_use_save_link) {
+				object_url = get_URL().createObjectURL(blob);
+				save_link.href = object_url;
+				save_link.download = name;
+				click(save_link);
+				filesaver.readyState = filesaver.DONE;
+				dispatch_all();
+				revoke(object_url);
+				return;
+			}
+			// Object and web filesystem URLs have a problem saving in Google Chrome when
+			// viewed in a tab, so I force save with application/octet-stream
+			// http://code.google.com/p/chromium/issues/detail?id=91158
+			// Update: Google errantly closed 91158, I submitted it again:
+			// https://code.google.com/p/chromium/issues/detail?id=389642
+			if (view.chrome && type && type !== force_saveable_type) {
+				slice = blob.slice || blob.webkitSlice;
+				blob = slice.call(blob, 0, blob.size, force_saveable_type);
+				blob_changed = true;
+			}
+			// Since I can't be sure that the guessed media type will trigger a download
+			// in WebKit, I append .download to the filename.
+			// https://bugs.webkit.org/show_bug.cgi?id=65440
+			if (webkit_req_fs && name !== "download") {
+				name += ".download";
+			}
+			if (type === force_saveable_type || webkit_req_fs) {
+				target_view = view;
+			}
+			if (!req_fs) {
+				fs_error();
+				return;
+			}
+			fs_min_size += blob.size;
+			req_fs(view.TEMPORARY, fs_min_size, abortable(function(fs) {
+				fs.root.getDirectory("saved", create_if_not_found, abortable(function(dir) {
+					var save = function() {
+						dir.getFile(name, create_if_not_found, abortable(function(file) {
+							file.createWriter(abortable(function(writer) {
+								writer.onwriteend = function(event) {
+									target_view.location.href = file.toURL();
+									filesaver.readyState = filesaver.DONE;
+									dispatch(filesaver, "writeend", event);
+									revoke(file);
+								};
+								writer.onerror = function() {
+									var error = writer.error;
+									if (error.code !== error.ABORT_ERR) {
+										fs_error();
+									}
+								};
+								"writestart progress write abort".split(" ").forEach(function(event) {
+									writer["on" + event] = filesaver["on" + event];
+								});
+								writer.write(blob);
+								filesaver.abort = function() {
+									writer.abort();
+									filesaver.readyState = filesaver.DONE;
+								};
+								filesaver.readyState = filesaver.WRITING;
+							}), fs_error);
+						}), fs_error);
+					};
+					dir.getFile(name, {create: false}, abortable(function(file) {
+						// delete file if it already exists
+						file.remove();
+						save();
+					}), abortable(function(ex) {
+						if (ex.code === ex.NOT_FOUND_ERR) {
+							save();
+						} else {
+							fs_error();
+						}
+					}));
+				}), fs_error);
+			}), fs_error);
+		}
+		, FS_proto = FileSaver.prototype
+		, saveAs = function(blob, name) {
+			return new FileSaver(blob, name);
+		}
+	;
+	FS_proto.abort = function() {
+		var filesaver = this;
+		filesaver.readyState = filesaver.DONE;
+		dispatch(filesaver, "abort");
+	};
+	FS_proto.readyState = FS_proto.INIT = 0;
+	FS_proto.WRITING = 1;
+	FS_proto.DONE = 2;
+
+	FS_proto.error =
+	FS_proto.onwritestart =
+	FS_proto.onprogress =
+	FS_proto.onwrite =
+	FS_proto.onabort =
+	FS_proto.onerror =
+	FS_proto.onwriteend =
+		null;
+
+	return saveAs;
+}(
+	   typeof self !== "undefined" && self
+	|| typeof window !== "undefined" && window
+	|| this.content
+));
+// `self` is undefined in Firefox for Android content script context
+// while `this` is nsIContentFrameMessageManager
+// with an attribute `content` that corresponds to the window
+
+if (typeof module !== "undefined" && module !== null) {
+  module.exports = saveAs;
+} else if ((typeof define !== "undefined" && 0)) {
+  define([], function() {
+    return saveAs;
+  });
+}
+/*
+ * Copyright (c) 2012 chick307 <chick307@gmail.com>
+ *
+ * Licensed under the MIT License.
+ * http://opensource.org/licenses/mit-license
+ */
+
+void function(global, callback) {
+	if (typeof module === 'object') {
+		module.exports = callback();
+	} else if (0 === 'function') {
+		define(callback);
+	} else {
+		global.adler32cs = callback();
+	}
+}(jsPDF, function() {
+	var _hasArrayBuffer = typeof ArrayBuffer === 'function' &&
+		typeof Uint8Array === 'function';
+
+	var _Buffer = null, _isBuffer = (function() {
+		if (!_hasArrayBuffer)
+			return function _isBuffer() { return false };
+
+		try {
+			var buffer = require('buffer');
+			if (typeof buffer.Buffer === 'function')
+				_Buffer = buffer.Buffer;
+		} catch (error) {}
+
+		return function _isBuffer(value) {
+			return value instanceof ArrayBuffer ||
+				_Buffer !== null && value instanceof _Buffer;
+		};
+	}());
+
+	var _utf8ToBinary = (function() {
+		if (_Buffer !== null) {
+			return function _utf8ToBinary(utf8String) {
+				return new _Buffer(utf8String, 'utf8').toString('binary');
+			};
+		} else {
+			return function _utf8ToBinary(utf8String) {
+				return unescape(encodeURIComponent(utf8String));
+			};
+		}
+	}());
+
+	var MOD = 65521;
+
+	var _update = function _update(checksum, binaryString) {
+		var a = checksum & 0xFFFF, b = checksum >>> 16;
+		for (var i = 0, length = binaryString.length; i < length; i++) {
+			a = (a + (binaryString.charCodeAt(i) & 0xFF)) % MOD;
+			b = (b + a) % MOD;
+		}
+		return (b << 16 | a) >>> 0;
+	};
+
+	var _updateUint8Array = function _updateUint8Array(checksum, uint8Array) {
+		var a = checksum & 0xFFFF, b = checksum >>> 16;
+		for (var i = 0, length = uint8Array.length, x; i < length; i++) {
+			a = (a + uint8Array[i]) % MOD;
+			b = (b + a) % MOD;
+		}
+		return (b << 16 | a) >>> 0
+	};
+
+	var exports = {};
+
+	var Adler32 = exports.Adler32 = (function() {
+		var ctor = function Adler32(checksum) {
+			if (!(this instanceof ctor)) {
+				throw new TypeError(
+					'Constructor cannot called be as a function.');
+			}
+			if (!isFinite(checksum = checksum == null ? 1 : +checksum)) {
+				throw new Error(
+					'First arguments needs to be a finite number.');
+			}
+			this.checksum = checksum >>> 0;
+		};
+
+		var proto = ctor.prototype = {};
+		proto.constructor = ctor;
+
+		ctor.from = function(from) {
+			from.prototype = proto;
+			return from;
+		}(function from(binaryString) {
+			if (!(this instanceof ctor)) {
+				throw new TypeError(
+					'Constructor cannot called be as a function.');
+			}
+			if (binaryString == null)
+				throw new Error('First argument needs to be a string.');
+			this.checksum = _update(1, binaryString.toString());
+		});
+
+		ctor.fromUtf8 = function(fromUtf8) {
+			fromUtf8.prototype = proto;
+			return fromUtf8;
+		}(function fromUtf8(utf8String) {
+			if (!(this instanceof ctor)) {
+				throw new TypeError(
+					'Constructor cannot called be as a function.');
+			}
+			if (utf8String == null)
+				throw new Error('First argument needs to be a string.');
+			var binaryString = _utf8ToBinary(utf8String.toString());
+			this.checksum = _update(1, binaryString);
+		});
+
+		if (_hasArrayBuffer) {
+			ctor.fromBuffer = function(fromBuffer) {
+				fromBuffer.prototype = proto;
+				return fromBuffer;
+			}(function fromBuffer(buffer) {
+				if (!(this instanceof ctor)) {
+					throw new TypeError(
+						'Constructor cannot called be as a function.');
+				}
+				if (!_isBuffer(buffer))
+					throw new Error('First argument needs to be ArrayBuffer.');
+				var array = new Uint8Array(buffer);
+				return this.checksum = _updateUint8Array(1, array);
+			});
+		}
+
+		proto.update = function update(binaryString) {
+			if (binaryString == null)
+				throw new Error('First argument needs to be a string.');
+			binaryString = binaryString.toString();
+			return this.checksum = _update(this.checksum, binaryString);
+		};
+
+		proto.updateUtf8 = function updateUtf8(utf8String) {
+			if (utf8String == null)
+				throw new Error('First argument needs to be a string.');
+			var binaryString = _utf8ToBinary(utf8String.toString());
+			return this.checksum = _update(this.checksum, binaryString);
+		};
+
+		if (_hasArrayBuffer) {
+			proto.updateBuffer = function updateBuffer(buffer) {
+				if (!_isBuffer(buffer))
+					throw new Error('First argument needs to be ArrayBuffer.');
+				var array = new Uint8Array(buffer);
+				return this.checksum = _updateUint8Array(this.checksum, array);
+			};
+		}
+
+		proto.clone = function clone() {
+			return new Adler32(this.checksum);
+		};
+
+		return ctor;
+	}());
+
+	exports.from = function from(binaryString) {
+		if (binaryString == null)
+			throw new Error('First argument needs to be a string.');
+		return _update(1, binaryString.toString());
+	};
+
+	exports.fromUtf8 = function fromUtf8(utf8String) {
+		if (utf8String == null)
+			throw new Error('First argument needs to be a string.');
+		var binaryString = _utf8ToBinary(utf8String.toString());
+		return _update(1, binaryString);
+	};
+
+	if (_hasArrayBuffer) {
+		exports.fromBuffer = function fromBuffer(buffer) {
+			if (!_isBuffer(buffer))
+				throw new Error('First argument need to be ArrayBuffer.');
+			var array = new Uint8Array(buffer);
+			return _updateUint8Array(1, array);
+		};
+	}
+
+	return exports;
+});
+/*
+ Deflate.js - https://github.com/gildas-lormeau/zip.js
+ Copyright (c) 2013 Gildas Lormeau. All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+
+ 1. Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above copyright 
+ notice, this list of conditions and the following disclaimer in 
+ the documentation and/or other materials provided with the distribution.
+
+ 3. The names of the authors may not be used to endorse or promote products
+ derived from this software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
+ INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+ FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT,
+ INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT,
+ INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
+ OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+ EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/*
+ * This program is based on JZlib 1.0.2 ymnk, JCraft,Inc.
+ * JZlib is based on zlib-1.1.3, so all credit should go authors
+ * Jean-loup Gailly(jloup@gzip.org) and Mark Adler(madler@alumni.caltech.edu)
+ * and contributors of zlib.
+ */
+
+var Deflater = (function(obj) {
+
+	// Global
+
+	var MAX_BITS = 15;
+	var D_CODES = 30;
+	var BL_CODES = 19;
+
+	var LENGTH_CODES = 29;
+	var LITERALS = 256;
+	var L_CODES = (LITERALS + 1 + LENGTH_CODES);
+	var HEAP_SIZE = (2 * L_CODES + 1);
+
+	var END_BLOCK = 256;
+
+	// Bit length codes must not exceed MAX_BL_BITS bits
+	var MAX_BL_BITS = 7;
+
+	// repeat previous bit length 3-6 times (2 bits of repeat count)
+	var REP_3_6 = 16;
+
+	// repeat a zero length 3-10 times (3 bits of repeat count)
+	var REPZ_3_10 = 17;
+
+	// repeat a zero length 11-138 times (7 bits of repeat count)
+	var REPZ_11_138 = 18;
+
+	// The lengths of the bit length codes are sent in order of decreasing
+	// probability, to avoid transmitting the lengths for unused bit
+	// length codes.
+
+	var Buf_size = 8 * 2;
+
+	// JZlib version : "1.0.2"
+	var Z_DEFAULT_COMPRESSION = -1;
+
+	// compression strategy
+	var Z_FILTERED = 1;
+	var Z_HUFFMAN_ONLY = 2;
+	var Z_DEFAULT_STRATEGY = 0;
+
+	var Z_NO_FLUSH = 0;
+	var Z_PARTIAL_FLUSH = 1;
+	var Z_FULL_FLUSH = 3;
+	var Z_FINISH = 4;
+
+	var Z_OK = 0;
+	var Z_STREAM_END = 1;
+	var Z_NEED_DICT = 2;
+	var Z_STREAM_ERROR = -2;
+	var Z_DATA_ERROR = -3;
+	var Z_BUF_ERROR = -5;
+
+	// Tree
+
+	// see definition of array dist_code below
+	var _dist_code = [ 0, 1, 2, 3, 4, 4, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
+			10, 10, 10, 10, 10, 10, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12,
+			12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13,
+			13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14,
+			14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14,
+			14, 14, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
+			15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 0, 0, 16, 17, 18, 18, 19, 19,
+			20, 20, 20, 20, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22, 22, 22, 23, 23, 23, 23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
+			24, 24, 24, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26,
+			26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27,
+			27, 27, 27, 27, 27, 27, 27, 27, 27, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28,
+			28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 29,
+			29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29,
+			29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29 ];
+
+	function Tree() {
+		var that = this;
+
+		// dyn_tree; // the dynamic tree
+		// max_code; // largest code with non zero frequency
+		// stat_desc; // the corresponding static tree
+
+		// Compute the optimal bit lengths for a tree and update the total bit
+		// length
+		// for the current block.
+		// IN assertion: the fields freq and dad are set, heap[heap_max] and
+		// above are the tree nodes sorted by increasing frequency.
+		// OUT assertions: the field len is set to the optimal bit length, the
+		// array bl_count contains the frequencies for each bit length.
+		// The length opt_len is updated; static_len is also updated if stree is
+		// not null.
+		function gen_bitlen(s) {
+			var tree = that.dyn_tree;
+			var stree = that.stat_desc.static_tree;
+			var extra = that.stat_desc.extra_bits;
+			var base = that.stat_desc.extra_base;
+			var max_length = that.stat_desc.max_length;
+			var h; // heap index
+			var n, m; // iterate over the tree elements
+			var bits; // bit length
+			var xbits; // extra bits
+			var f; // frequency
+			var overflow = 0; // number of elements with bit length too large
+
+			for (bits = 0; bits <= MAX_BITS; bits++)
+				s.bl_count[bits] = 0;
+
+			// In a first pass, compute the optimal bit lengths (which may
+			// overflow in the case of the bit length tree).
+			tree[s.heap[s.heap_max] * 2 + 1] = 0; // root of the heap
+
+			for (h = s.heap_max + 1; h < HEAP_SIZE; h++) {
+				n = s.heap[h];
+				bits = tree[tree[n * 2 + 1] * 2 + 1] + 1;
+				if (bits > max_length) {
+					bits = max_length;
+					overflow++;
+				}
+				tree[n * 2 + 1] = bits;
+				// We overwrite tree[n*2+1] which is no longer needed
+
+				if (n > that.max_code)
+					continue; // not a leaf node
+
+				s.bl_count[bits]++;
+				xbits = 0;
+				if (n >= base)
+					xbits = extra[n - base];
+				f = tree[n * 2];
+				s.opt_len += f * (bits + xbits);
+				if (stree)
+					s.static_len += f * (stree[n * 2 + 1] + xbits);
+			}
+			if (overflow === 0)
+				return;
+
+			// This happens for example on obj2 and pic of the Calgary corpus
+			// Find the first bit length which could increase:
+			do {
+				bits = max_length - 1;
+				while (s.bl_count[bits] === 0)
+					bits--;
+				s.bl_count[bits]--; // move one leaf down the tree
+				s.bl_count[bits + 1] += 2; // move one overflow item as its brother
+				s.bl_count[max_length]--;
+				// The brother of the overflow item also moves one step up,
+				// but this does not affect bl_count[max_length]
+				overflow -= 2;
+			} while (overflow > 0);
+
+			for (bits = max_length; bits !== 0; bits--) {
+				n = s.bl_count[bits];
+				while (n !== 0) {
+					m = s.heap[--h];
+					if (m > that.max_code)
+						continue;
+					if (tree[m * 2 + 1] != bits) {
+						s.opt_len += (bits - tree[m * 2 + 1]) * tree[m * 2];
+						tree[m * 2 + 1] = bits;
+					}
+					n--;
+				}
+			}
+		}
+
+		// Reverse the first len bits of a code, using straightforward code (a
+		// faster
+		// method would use a table)
+		// IN assertion: 1 <= len <= 15
+		function bi_reverse(code, // the value to invert
+		len // its bit length
+		) {
+			var res = 0;
+			do {
+				res |= code & 1;
+				code >>>= 1;
+				res <<= 1;
+			} while (--len > 0);
+			return res >>> 1;
+		}
+
+		// Generate the codes for a given tree and bit counts (which need not be
+		// optimal).
+		// IN assertion: the array bl_count contains the bit length statistics for
+		// the given tree and the field len is set for all tree elements.
+		// OUT assertion: the field code is set for all tree elements of non
+		// zero code length.
+		function gen_codes(tree, // the tree to decorate
+		max_code, // largest code with non zero frequency
+		bl_count // number of codes at each bit length
+		) {
+			var next_code = []; // next code value for each
+			// bit length
+			var code = 0; // running code value
+			var bits; // bit index
+			var n; // code index
+			var len;
+
+			// The distribution counts are first used to generate the code values
+			// without bit reversal.
+			for (bits = 1; bits <= MAX_BITS; bits++) {
+				next_code[bits] = code = ((code + bl_count[bits - 1]) << 1);
+			}
+
+			// Check that the bit counts in bl_count are consistent. The last code
+			// must be all ones.
+			// Assert (code + bl_count[MAX_BITS]-1 == (1<<MAX_BITS)-1,
+			// "inconsistent bit counts");
+			// Tracev((stderr,"\ngen_codes: max_code %d ", max_code));
+
+			for (n = 0; n <= max_code; n++) {
+				len = tree[n * 2 + 1];
+				if (len === 0)
+					continue;
+				// Now reverse the bits
+				tree[n * 2] = bi_reverse(next_code[len]++, len);
+			}
+		}
+
+		// Construct one Huffman tree and assigns the code bit strings and lengths.
+		// Update the total bit length for the current block.
+		// IN assertion: the field freq is set for all tree elements.
+		// OUT assertions: the fields len and code are set to the optimal bit length
+		// and corresponding code. The length opt_len is updated; static_len is
+		// also updated if stree is not null. The field max_code is set.
+		that.build_tree = function(s) {
+			var tree = that.dyn_tree;
+			var stree = that.stat_desc.static_tree;
+			var elems = that.stat_desc.elems;
+			var n, m; // iterate over heap elements
+			var max_code = -1; // largest code with non zero frequency
+			var node; // new node being created
+
+			// Construct the initial heap, with least frequent element in
+			// heap[1]. The sons of heap[n] are heap[2*n] and heap[2*n+1].
+			// heap[0] is not used.
+			s.heap_len = 0;
+			s.heap_max = HEAP_SIZE;
+
+			for (n = 0; n < elems; n++) {
+				if (tree[n * 2] !== 0) {
+					s.heap[++s.heap_len] = max_code = n;
+					s.depth[n] = 0;
+				} else {
+					tree[n * 2 + 1] = 0;
+				}
+			}
+
+			// The pkzip format requires that at least one distance code exists,
+			// and that at least one bit should be sent even if there is only one
+			// possible code. So to avoid special checks later on we force at least
+			// two codes of non zero frequency.
+			while (s.heap_len < 2) {
+				node = s.heap[++s.heap_len] = max_code < 2 ? ++max_code : 0;
+				tree[node * 2] = 1;
+				s.depth[node] = 0;
+				s.opt_len--;
+				if (stree)
+					s.static_len -= stree[node * 2 + 1];
+				// node is 0 or 1 so it does not have extra bits
+			}
+			that.max_code = max_code;
+
+			// The elements heap[heap_len/2+1 .. heap_len] are leaves of the tree,
+			// establish sub-heaps of increasing lengths:
+
+			for (n = Math.floor(s.heap_len / 2); n >= 1; n--)
+				s.pqdownheap(tree, n);
+
+			// Construct the Huffman tree by repeatedly combining the least two
+			// frequent nodes.
+
+			node = elems; // next internal node of the tree
+			do {
+				// n = node of least frequency
+				n = s.heap[1];
+				s.heap[1] = s.heap[s.heap_len--];
+				s.pqdownheap(tree, 1);
+				m = s.heap[1]; // m = node of next least frequency
+
+				s.heap[--s.heap_max] = n; // keep the nodes sorted by frequency
+				s.heap[--s.heap_max] = m;
+
+				// Create a new node father of n and m
+				tree[node * 2] = (tree[n * 2] + tree[m * 2]);
+				s.depth[node] = Math.max(s.depth[n], s.depth[m]) + 1;
+				tree[n * 2 + 1] = tree[m * 2 + 1] = node;
+
+				// and insert the new node in the heap
+				s.heap[1] = node++;
+				s.pqdownheap(tree, 1);
+			} while (s.heap_len >= 2);
+
+			s.heap[--s.heap_max] = s.heap[1];
+
+			// At this point, the fields freq and dad are set. We can now
+			// generate the bit lengths.
+
+			gen_bitlen(s);
+
+			// The field len is now set, we can generate the bit codes
+			gen_codes(tree, that.max_code, s.bl_count);
+		};
+
+	}
+
+	Tree._length_code = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 12, 12, 13, 13, 13, 13, 14, 14, 14, 14, 15, 15, 15, 15, 16, 16, 16, 16,
+			16, 16, 16, 16, 17, 17, 17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 18, 18, 19, 19, 19, 19, 19, 19, 19, 19, 20, 20, 20, 20, 20, 20, 20, 20, 20,
+			20, 20, 20, 20, 20, 20, 20, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22,
+			22, 22, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
+			24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
+			25, 25, 25, 25, 25, 25, 25, 25, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26,
+			26, 26, 26, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 28 ];
+
+	Tree.base_length = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 12, 14, 16, 20, 24, 28, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 0 ];
+
+	Tree.base_dist = [ 0, 1, 2, 3, 4, 6, 8, 12, 16, 24, 32, 48, 64, 96, 128, 192, 256, 384, 512, 768, 1024, 1536, 2048, 3072, 4096, 6144, 8192, 12288, 16384,
+			24576 ];
+
+	// Mapping from a distance to a distance code. dist is the distance - 1 and
+	// must not have side effects. _dist_code[256] and _dist_code[257] are never
+	// used.
+	Tree.d_code = function(dist) {
+		return ((dist) < 256 ? _dist_code[dist] : _dist_code[256 + ((dist) >>> 7)]);
+	};
+
+	// extra bits for each length code
+	Tree.extra_lbits = [ 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0 ];
+
+	// extra bits for each distance code
+	Tree.extra_dbits = [ 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13 ];
+
+	// extra bits for each bit length code
+	Tree.extra_blbits = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 3, 7 ];
+
+	Tree.bl_order = [ 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 ];
+
+	// StaticTree
+
+	function StaticTree(static_tree, extra_bits, extra_base, elems, max_length) {
+		var that = this;
+		that.static_tree = static_tree;
+		that.extra_bits = extra_bits;
+		that.extra_base = extra_base;
+		that.elems = elems;
+		that.max_length = max_length;
+	}
+
+	StaticTree.static_ltree = [ 12, 8, 140, 8, 76, 8, 204, 8, 44, 8, 172, 8, 108, 8, 236, 8, 28, 8, 156, 8, 92, 8, 220, 8, 60, 8, 188, 8, 124, 8, 252, 8, 2, 8,
+			130, 8, 66, 8, 194, 8, 34, 8, 162, 8, 98, 8, 226, 8, 18, 8, 146, 8, 82, 8, 210, 8, 50, 8, 178, 8, 114, 8, 242, 8, 10, 8, 138, 8, 74, 8, 202, 8, 42,
+			8, 170, 8, 106, 8, 234, 8, 26, 8, 154, 8, 90, 8, 218, 8, 58, 8, 186, 8, 122, 8, 250, 8, 6, 8, 134, 8, 70, 8, 198, 8, 38, 8, 166, 8, 102, 8, 230, 8,
+			22, 8, 150, 8, 86, 8, 214, 8, 54, 8, 182, 8, 118, 8, 246, 8, 14, 8, 142, 8, 78, 8, 206, 8, 46, 8, 174, 8, 110, 8, 238, 8, 30, 8, 158, 8, 94, 8,
+			222, 8, 62, 8, 190, 8, 126, 8, 254, 8, 1, 8, 129, 8, 65, 8, 193, 8, 33, 8, 161, 8, 97, 8, 225, 8, 17, 8, 145, 8, 81, 8, 209, 8, 49, 8, 177, 8, 113,
+			8, 241, 8, 9, 8, 137, 8, 73, 8, 201, 8, 41, 8, 169, 8, 105, 8, 233, 8, 25, 8, 153, 8, 89, 8, 217, 8, 57, 8, 185, 8, 121, 8, 249, 8, 5, 8, 133, 8,
+			69, 8, 197, 8, 37, 8, 165, 8, 101, 8, 229, 8, 21, 8, 149, 8, 85, 8, 213, 8, 53, 8, 181, 8, 117, 8, 245, 8, 13, 8, 141, 8, 77, 8, 205, 8, 45, 8,
+			173, 8, 109, 8, 237, 8, 29, 8, 157, 8, 93, 8, 221, 8, 61, 8, 189, 8, 125, 8, 253, 8, 19, 9, 275, 9, 147, 9, 403, 9, 83, 9, 339, 9, 211, 9, 467, 9,
+			51, 9, 307, 9, 179, 9, 435, 9, 115, 9, 371, 9, 243, 9, 499, 9, 11, 9, 267, 9, 139, 9, 395, 9, 75, 9, 331, 9, 203, 9, 459, 9, 43, 9, 299, 9, 171, 9,
+			427, 9, 107, 9, 363, 9, 235, 9, 491, 9, 27, 9, 283, 9, 155, 9, 411, 9, 91, 9, 347, 9, 219, 9, 475, 9, 59, 9, 315, 9, 187, 9, 443, 9, 123, 9, 379,
+			9, 251, 9, 507, 9, 7, 9, 263, 9, 135, 9, 391, 9, 71, 9, 327, 9, 199, 9, 455, 9, 39, 9, 295, 9, 167, 9, 423, 9, 103, 9, 359, 9, 231, 9, 487, 9, 23,
+			9, 279, 9, 151, 9, 407, 9, 87, 9, 343, 9, 215, 9, 471, 9, 55, 9, 311, 9, 183, 9, 439, 9, 119, 9, 375, 9, 247, 9, 503, 9, 15, 9, 271, 9, 143, 9,
+			399, 9, 79, 9, 335, 9, 207, 9, 463, 9, 47, 9, 303, 9, 175, 9, 431, 9, 111, 9, 367, 9, 239, 9, 495, 9, 31, 9, 287, 9, 159, 9, 415, 9, 95, 9, 351, 9,
+			223, 9, 479, 9, 63, 9, 319, 9, 191, 9, 447, 9, 127, 9, 383, 9, 255, 9, 511, 9, 0, 7, 64, 7, 32, 7, 96, 7, 16, 7, 80, 7, 48, 7, 112, 7, 8, 7, 72, 7,
+			40, 7, 104, 7, 24, 7, 88, 7, 56, 7, 120, 7, 4, 7, 68, 7, 36, 7, 100, 7, 20, 7, 84, 7, 52, 7, 116, 7, 3, 8, 131, 8, 67, 8, 195, 8, 35, 8, 163, 8,
+			99, 8, 227, 8 ];
+
+	StaticTree.static_dtree = [ 0, 5, 16, 5, 8, 5, 24, 5, 4, 5, 20, 5, 12, 5, 28, 5, 2, 5, 18, 5, 10, 5, 26, 5, 6, 5, 22, 5, 14, 5, 30, 5, 1, 5, 17, 5, 9, 5,
+			25, 5, 5, 5, 21, 5, 13, 5, 29, 5, 3, 5, 19, 5, 11, 5, 27, 5, 7, 5, 23, 5 ];
+
+	StaticTree.static_l_desc = new StaticTree(StaticTree.static_ltree, Tree.extra_lbits, LITERALS + 1, L_CODES, MAX_BITS);
+
+	StaticTree.static_d_desc = new StaticTree(StaticTree.static_dtree, Tree.extra_dbits, 0, D_CODES, MAX_BITS);
+
+	StaticTree.static_bl_desc = new StaticTree(null, Tree.extra_blbits, 0, BL_CODES, MAX_BL_BITS);
+
+	// Deflate
+
+	var MAX_MEM_LEVEL = 9;
+	var DEF_MEM_LEVEL = 8;
+
+	function Config(good_length, max_lazy, nice_length, max_chain, func) {
+		var that = this;
+		that.good_length = good_length;
+		that.max_lazy = max_lazy;
+		that.nice_length = nice_length;
+		that.max_chain = max_chain;
+		that.func = func;
+	}
+
+	var STORED = 0;
+	var FAST = 1;
+	var SLOW = 2;
+	var config_table = [ new Config(0, 0, 0, 0, STORED), new Config(4, 4, 8, 4, FAST), new Config(4, 5, 16, 8, FAST), new Config(4, 6, 32, 32, FAST),
+			new Config(4, 4, 16, 16, SLOW), new Config(8, 16, 32, 32, SLOW), new Config(8, 16, 128, 128, SLOW), new Config(8, 32, 128, 256, SLOW),
+			new Config(32, 128, 258, 1024, SLOW), new Config(32, 258, 258, 4096, SLOW) ];
+
+	var z_errmsg = [ "need dictionary", // Z_NEED_DICT
+	// 2
+	"stream end", // Z_STREAM_END 1
+	"", // Z_OK 0
+	"", // Z_ERRNO (-1)
+	"stream error", // Z_STREAM_ERROR (-2)
+	"data error", // Z_DATA_ERROR (-3)
+	"", // Z_MEM_ERROR (-4)
+	"buffer error", // Z_BUF_ERROR (-5)
+	"",// Z_VERSION_ERROR (-6)
+	"" ];
+
+	// block not completed, need more input or more output
+	var NeedMore = 0;
+
+	// block flush performed
+	var BlockDone = 1;
+
+	// finish started, need only more output at next deflate
+	var FinishStarted = 2;
+
+	// finish done, accept no more input or output
+	var FinishDone = 3;
+
+	// preset dictionary flag in zlib header
+	var PRESET_DICT = 0x20;
+
+	var INIT_STATE = 42;
+	var BUSY_STATE = 113;
+	var FINISH_STATE = 666;
+
+	// The deflate compression method
+	var Z_DEFLATED = 8;
+
+	var STORED_BLOCK = 0;
+	var STATIC_TREES = 1;
+	var DYN_TREES = 2;
+
+	var MIN_MATCH = 3;
+	var MAX_MATCH = 258;
+	var MIN_LOOKAHEAD = (MAX_MATCH + MIN_MATCH + 1);
+
+	function smaller(tree, n, m, depth) {
+		var tn2 = tree[n * 2];
+		var tm2 = tree[m * 2];
+		return (tn2 < tm2 || (tn2 == tm2 && depth[n] <= depth[m]));
+	}
+
+	function Deflate() {
+
+		var that = this;
+		var strm; // pointer back to this zlib stream
+		var status; // as the name implies
+		// pending_buf; // output still pending
+		var pending_buf_size; // size of pending_buf
+		// pending_out; // next pending byte to output to the stream
+		// pending; // nb of bytes in the pending buffer
+		var method; // STORED (for zip only) or DEFLATED
+		var last_flush; // value of flush param for previous deflate call
+
+		var w_size; // LZ77 window size (32K by default)
+		var w_bits; // log2(w_size) (8..16)
+		var w_mask; // w_size - 1
+
+		var window;
+		// Sliding window. Input bytes are read into the second half of the window,
+		// and move to the first half later to keep a dictionary of at least wSize
+		// bytes. With this organization, matches are limited to a distance of
+		// wSize-MAX_MATCH bytes, but this ensures that IO is always
+		// performed with a length multiple of the block size. Also, it limits
+		// the window size to 64K, which is quite useful on MSDOS.
+		// To do: use the user input buffer as sliding window.
+
+		var window_size;
+		// Actual size of window: 2*wSize, except when the user input buffer
+		// is directly used as sliding window.
+
+		var prev;
+		// Link to older string with same hash index. To limit the size of this
+		// array to 64K, this link is maintained only for the last 32K strings.
+		// An index in this array is thus a window index modulo 32K.
+
+		var head; // Heads of the hash chains or NIL.
+
+		var ins_h; // hash index of string to be inserted
+		var hash_size; // number of elements in hash table
+		var hash_bits; // log2(hash_size)
+		var hash_mask; // hash_size-1
+
+		// Number of bits by which ins_h must be shifted at each input
+		// step. It must be such that after MIN_MATCH steps, the oldest
+		// byte no longer takes part in the hash key, that is:
+		// hash_shift * MIN_MATCH >= hash_bits
+		var hash_shift;
+
+		// Window position at the beginning of the current output block. Gets
+		// negative when the window is moved backwards.
+
+		var block_start;
+
+		var match_length; // length of best match
+		var prev_match; // previous match
+		var match_available; // set if previous match exists
+		var strstart; // start of string to insert
+		var match_start; // start of matching string
+		var lookahead; // number of valid bytes ahead in window
+
+		// Length of the best match at previous step. Matches not greater than this
+		// are discarded. This is used in the lazy match evaluation.
+		var prev_length;
+
+		// To speed up deflation, hash chains are never searched beyond this
+		// length. A higher limit improves compression ratio but degrades the speed.
+		var max_chain_length;
+
+		// Attempt to find a better match only when the current match is strictly
+		// smaller than this value. This mechanism is used only for compression
+		// levels >= 4.
+		var max_lazy_match;
+
+		// Insert new strings in the hash table only if the match length is not
+		// greater than this length. This saves time but degrades compression.
+		// max_insert_length is used only for compression levels <= 3.
+
+		var level; // compression level (1..9)
+		var strategy; // favor or force Huffman coding
+
+		// Use a faster search when the previous match is longer than this
+		var good_match;
+
+		// Stop searching when current match exceeds this
+		var nice_match;
+
+		var dyn_ltree; // literal and length tree
+		var dyn_dtree; // distance tree
+		var bl_tree; // Huffman tree for bit lengths
+
+		var l_desc = new Tree(); // desc for literal tree
+		var d_desc = new Tree(); // desc for distance tree
+		var bl_desc = new Tree(); // desc for bit length tree
+
+		// that.heap_len; // number of elements in the heap
+		// that.heap_max; // element of largest frequency
+		// The sons of heap[n] are heap[2*n] and heap[2*n+1]. heap[0] is not used.
+		// The same heap array is used to build all trees.
+
+		// Depth of each subtree used as tie breaker for trees of equal frequency
+		that.depth = [];
+
+		var l_buf; // index for literals or lengths */
+
+		// Size of match buffer for literals/lengths. There are 4 reasons for
+		// limiting lit_bufsize to 64K:
+		// - frequencies can be kept in 16 bit counters
+		// - if compression is not successful for the first block, all input
+		// data is still in the window so we can still emit a stored block even
+		// when input comes from standard input. (This can also be done for
+		// all blocks if lit_bufsize is not greater than 32K.)
+		// - if compression is not successful for a file smaller than 64K, we can
+		// even emit a stored file instead of a stored block (saving 5 bytes).
+		// This is applicable only for zip (not gzip or zlib).
+		// - creating new Huffman trees less frequently may not provide fast
+		// adaptation to changes in the input data statistics. (Take for
+		// example a binary file with poorly compressible code followed by
+		// a highly compressible string table.) Smaller buffer sizes give
+		// fast adaptation but have of course the overhead of transmitting
+		// trees more frequently.
+		// - I can't count above 4
+		var lit_bufsize;
+
+		var last_lit; // running index in l_buf
+
+		// Buffer for distances. To simplify the code, d_buf and l_buf have
+		// the same number of elements. To use different lengths, an extra flag
+		// array would be necessary.
+
+		var d_buf; // index of pendig_buf
+
+		// that.opt_len; // bit length of current block with optimal trees
+		// that.static_len; // bit length of current block with static trees
+		var matches; // number of string matches in current block
+		var last_eob_len; // bit length of EOB code for last block
+
+		// Output buffer. bits are inserted starting at the bottom (least
+		// significant bits).
+		var bi_buf;
+
+		// Number of valid bits in bi_buf. All bits above the last valid bit
+		// are always zero.
+		var bi_valid;
+
+		// number of codes at each bit length for an optimal tree
+		that.bl_count = [];
+
+		// heap used to build the Huffman trees
+		that.heap = [];
+
+		dyn_ltree = [];
+		dyn_dtree = [];
+		bl_tree = [];
+
+		function lm_init() {
+			var i;
+			window_size = 2 * w_size;
+
+			head[hash_size - 1] = 0;
+			for (i = 0; i < hash_size - 1; i++) {
+				head[i] = 0;
+			}
+
+			// Set the default configuration parameters:
+			max_lazy_match = config_table[level].max_lazy;
+			good_match = config_table[level].good_length;
+			nice_match = config_table[level].nice_length;
+			max_chain_length = config_table[level].max_chain;
+
+			strstart = 0;
+			block_start = 0;
+			lookahead = 0;
+			match_length = prev_length = MIN_MATCH - 1;
+			match_available = 0;
+			ins_h = 0;
+		}
+
+		function init_block() {
+			var i;
+			// Initialize the trees.
+			for (i = 0; i < L_CODES; i++)
+				dyn_ltree[i * 2] = 0;
+			for (i = 0; i < D_CODES; i++)
+				dyn_dtree[i * 2] = 0;
+			for (i = 0; i < BL_CODES; i++)
+				bl_tree[i * 2] = 0;
+
+			dyn_ltree[END_BLOCK * 2] = 1;
+			that.opt_len = that.static_len = 0;
+			last_lit = matches = 0;
+		}
+
+		// Initialize the tree data structures for a new zlib stream.
+		function tr_init() {
+
+			l_desc.dyn_tree = dyn_ltree;
+			l_desc.stat_desc = StaticTree.static_l_desc;
+
+			d_desc.dyn_tree = dyn_dtree;
+			d_desc.stat_desc = StaticTree.static_d_desc;
+
+			bl_desc.dyn_tree = bl_tree;
+			bl_desc.stat_desc = StaticTree.static_bl_desc;
+
+			bi_buf = 0;
+			bi_valid = 0;
+			last_eob_len = 8; // enough lookahead for inflate
+
+			// Initialize the first block of the first file:
+			init_block();
+		}
+
+		// Restore the heap property by moving down the tree starting at node k,
+		// exchanging a node with the smallest of its two sons if necessary,
+		// stopping
+		// when the heap property is re-established (each father smaller than its
+		// two sons).
+		that.pqdownheap = function(tree, // the tree to restore
+		k // node to move down
+		) {
+			var heap = that.heap;
+			var v = heap[k];
+			var j = k << 1; // left son of k
+			while (j <= that.heap_len) {
+				// Set j to the smallest of the two sons:
+				if (j < that.heap_len && smaller(tree, heap[j + 1], heap[j], that.depth)) {
+					j++;
+				}
+				// Exit if v is smaller than both sons
+				if (smaller(tree, v, heap[j], that.depth))
+					break;
+
+				// Exchange v with the smallest son
+				heap[k] = heap[j];
+				k = j;
+				// And continue down the tree, setting j to the left son of k
+				j <<= 1;
+			}
+			heap[k] = v;
+		};
+
+		// Scan a literal or distance tree to determine the frequencies of the codes
+		// in the bit length tree.
+		function scan_tree(tree,// the tree to be scanned
+		max_code // and its largest code of non zero frequency
+		) {
+			var n; // iterates over all tree elements
+			var prevlen = -1; // last emitted length
+			var curlen; // length of current code
+			var nextlen = tree[0 * 2 + 1]; // length of next code
+			var count = 0; // repeat count of the current code
+			var max_count = 7; // max repeat count
+			var min_count = 4; // min repeat count
+
+			if (nextlen === 0) {
+				max_count = 138;
+				min_count = 3;
+			}
+			tree[(max_code + 1) * 2 + 1] = 0xffff; // guard
+
+			for (n = 0; n <= max_code; n++) {
+				curlen = nextlen;
+				nextlen = tree[(n + 1) * 2 + 1];
+				if (++count < max_count && curlen == nextlen) {
+					continue;
+				} else if (count < min_count) {
+					bl_tree[curlen * 2] += count;
+				} else if (curlen !== 0) {
+					if (curlen != prevlen)
+						bl_tree[curlen * 2]++;
+					bl_tree[REP_3_6 * 2]++;
+				} else if (count <= 10) {
+					bl_tree[REPZ_3_10 * 2]++;
+				} else {
+					bl_tree[REPZ_11_138 * 2]++;
+				}
+				count = 0;
+				prevlen = curlen;
+				if (nextlen === 0) {
+					max_count = 138;
+					min_count = 3;
+				} else if (curlen == nextlen) {
+					max_count = 6;
+					min_count = 3;
+				} else {
+					max_count = 7;
+					min_count = 4;
+				}
+			}
+		}
+
+		// Construct the Huffman tree for the bit lengths and return the index in
+		// bl_order of the last bit length code to send.
+		function build_bl_tree() {
+			var max_blindex; // index of last bit length code of non zero freq
+
+			// Determine the bit length frequencies for literal and distance trees
+			scan_tree(dyn_ltree, l_desc.max_code);
+			scan_tree(dyn_dtree, d_desc.max_code);
+
+			// Build the bit length tree:
+			bl_desc.build_tree(that);
+			// opt_len now includes the length of the tree representations, except
+			// the lengths of the bit lengths codes and the 5+5+4 bits for the
+			// counts.
+
+			// Determine the number of bit length codes to send. The pkzip format
+			// requires that at least 4 bit length codes be sent. (appnote.txt says
+			// 3 but the actual value used is 4.)
+			for (max_blindex = BL_CODES - 1; max_blindex >= 3; max_blindex--) {
+				if (bl_tree[Tree.bl_order[max_blindex] * 2 + 1] !== 0)
+					break;
+			}
+			// Update opt_len to include the bit length tree and counts
+			that.opt_len += 3 * (max_blindex + 1) + 5 + 5 + 4;
+
+			return max_blindex;
+		}
+
+		// Output a byte on the stream.
+		// IN assertion: there is enough room in pending_buf.
+		function put_byte(p) {
+			that.pending_buf[that.pending++] = p;
+		}
+
+		function put_short(w) {
+			put_byte(w & 0xff);
+			put_byte((w >>> 8) & 0xff);
+		}
+
+		function putShortMSB(b) {
+			put_byte((b >> 8) & 0xff);
+			put_byte((b & 0xff) & 0xff);
+		}
+
+		function send_bits(value, length) {
+			var val, len = length;
+			if (bi_valid > Buf_size - len) {
+				val = value;
+				// bi_buf |= (val << bi_valid);
+				bi_buf |= ((val << bi_valid) & 0xffff);
+				put_short(bi_buf);
+				bi_buf = val >>> (Buf_size - bi_valid);
+				bi_valid += len - Buf_size;
+			} else {
+				// bi_buf |= (value) << bi_valid;
+				bi_buf |= (((value) << bi_valid) & 0xffff);
+				bi_valid += len;
+			}
+		}
+
+		function send_code(c, tree) {
+			var c2 = c * 2;
+			send_bits(tree[c2] & 0xffff, tree[c2 + 1] & 0xffff);
+		}
+
+		// Send a literal or distance tree in compressed form, using the codes in
+		// bl_tree.
+		function send_tree(tree,// the tree to be sent
+		max_code // and its largest code of non zero frequency
+		) {
+			var n; // iterates over all tree elements
+			var prevlen = -1; // last emitted length
+			var curlen; // length of current code
+			var nextlen = tree[0 * 2 + 1]; // length of next code
+			var count = 0; // repeat count of the current code
+			var max_count = 7; // max repeat count
+			var min_count = 4; // min repeat count
+
+			if (nextlen === 0) {
+				max_count = 138;
+				min_count = 3;
+			}
+
+			for (n = 0; n <= max_code; n++) {
+				curlen = nextlen;
+				nextlen = tree[(n + 1) * 2 + 1];
+				if (++count < max_count && curlen == nextlen) {
+					continue;
+				} else if (count < min_count) {
+					do {
+						send_code(curlen, bl_tree);
+					} while (--count !== 0);
+				} else if (curlen !== 0) {
+					if (curlen != prevlen) {
+						send_code(curlen, bl_tree);
+						count--;
+					}
+					send_code(REP_3_6, bl_tree);
+					send_bits(count - 3, 2);
+				} else if (count <= 10) {
+					send_code(REPZ_3_10, bl_tree);
+					send_bits(count - 3, 3);
+				} else {
+					send_code(REPZ_11_138, bl_tree);
+					send_bits(count - 11, 7);
+				}
+				count = 0;
+				prevlen = curlen;
+				if (nextlen === 0) {
+					max_count = 138;
+					min_count = 3;
+				} else if (curlen == nextlen) {
+					max_count = 6;
+					min_count = 3;
+				} else {
+					max_count = 7;
+					min_count = 4;
+				}
+			}
+		}
+
+		// Send the header for a block using dynamic Huffman trees: the counts, the
+		// lengths of the bit length codes, the literal tree and the distance tree.
+		// IN assertion: lcodes >= 257, dcodes >= 1, blcodes >= 4.
+		function send_all_trees(lcodes, dcodes, blcodes) {
+			var rank; // index in bl_order
+
+			send_bits(lcodes - 257, 5); // not +255 as stated in appnote.txt
+			send_bits(dcodes - 1, 5);
+			send_bits(blcodes - 4, 4); // not -3 as stated in appnote.txt
+			for (rank = 0; rank < blcodes; rank++) {
+				send_bits(bl_tree[Tree.bl_order[rank] * 2 + 1], 3);
+			}
+			send_tree(dyn_ltree, lcodes - 1); // literal tree
+			send_tree(dyn_dtree, dcodes - 1); // distance tree
+		}
+
+		// Flush the bit buffer, keeping at most 7 bits in it.
+		function bi_flush() {
+			if (bi_valid == 16) {
+				put_short(bi_buf);
+				bi_buf = 0;
+				bi_valid = 0;
+			} else if (bi_valid >= 8) {
+				put_byte(bi_buf & 0xff);
+				bi_buf >>>= 8;
+				bi_valid -= 8;
+			}
+		}
+
+		// Send one empty static block to give enough lookahead for inflate.
+		// This takes 10 bits, of which 7 may remain in the bit buffer.
+		// The current inflate code requires 9 bits of lookahead. If the
+		// last two codes for the previous block (real code plus EOB) were coded
+		// on 5 bits or less, inflate may have only 5+3 bits of lookahead to decode
+		// the last real code. In this case we send two empty static blocks instead
+		// of one. (There are no problems if the previous block is stored or fixed.)
+		// To simplify the code, we assume the worst case of last real code encoded
+		// on one bit only.
+		function _tr_align() {
+			send_bits(STATIC_TREES << 1, 3);
+			send_code(END_BLOCK, StaticTree.static_ltree);
+
+			bi_flush();
+
+			// Of the 10 bits for the empty block, we have already sent
+			// (10 - bi_valid) bits. The lookahead for the last real code (before
+			// the EOB of the previous block) was thus at least one plus the length
+			// of the EOB plus what we have just sent of the empty static block.
+			if (1 + last_eob_len + 10 - bi_valid < 9) {
+				send_bits(STATIC_TREES << 1, 3);
+				send_code(END_BLOCK, StaticTree.static_ltree);
+				bi_flush();
+			}
+			last_eob_len = 7;
+		}
+
+		// Save the match info and tally the frequency counts. Return true if
+		// the current block must be flushed.
+		function _tr_tally(dist, // distance of matched string
+		lc // match length-MIN_MATCH or unmatched char (if dist==0)
+		) {
+			var out_length, in_length, dcode;
+			that.pending_buf[d_buf + last_lit * 2] = (dist >>> 8) & 0xff;
+			that.pending_buf[d_buf + last_lit * 2 + 1] = dist & 0xff;
+
+			that.pending_buf[l_buf + last_lit] = lc & 0xff;
+			last_lit++;
+
+			if (dist === 0) {
+				// lc is the unmatched char
+				dyn_ltree[lc * 2]++;
+			} else {
+				matches++;
+				// Here, lc is the match length - MIN_MATCH
+				dist--; // dist = match distance - 1
+				dyn_ltree[(Tree._length_code[lc] + LITERALS + 1) * 2]++;
+				dyn_dtree[Tree.d_code(dist) * 2]++;
+			}
+
+			if ((last_lit & 0x1fff) === 0 && level > 2) {
+				// Compute an upper bound for the compressed length
+				out_length = last_lit * 8;
+				in_length = strstart - block_start;
+				for (dcode = 0; dcode < D_CODES; dcode++) {
+					out_length += dyn_dtree[dcode * 2] * (5 + Tree.extra_dbits[dcode]);
+				}
+				out_length >>>= 3;
+				if ((matches < Math.floor(last_lit / 2)) && out_length < Math.floor(in_length / 2))
+					return true;
+			}
+
+			return (last_lit == lit_bufsize - 1);
+			// We avoid equality with lit_bufsize because of wraparound at 64K
+			// on 16 bit machines and because stored blocks are restricted to
+			// 64K-1 bytes.
+		}
+
+		// Send the block data compressed using the given Huffman trees
+		function compress_block(ltree, dtree) {
+			var dist; // distance of matched string
+			var lc; // match length or unmatched char (if dist === 0)
+			var lx = 0; // running index in l_buf
+			var code; // the code to send
+			var extra; // number of extra bits to send
+
+			if (last_lit !== 0) {
+				do {
+					dist = ((that.pending_buf[d_buf + lx * 2] << 8) & 0xff00) | (that.pending_buf[d_buf + lx * 2 + 1] & 0xff);
+					lc = (that.pending_buf[l_buf + lx]) & 0xff;
+					lx++;
+
+					if (dist === 0) {
+						send_code(lc, ltree); // send a literal byte
+					} else {
+						// Here, lc is the match length - MIN_MATCH
+						code = Tree._length_code[lc];
+
+						send_code(code + LITERALS + 1, ltree); // send the length
+						// code
+						extra = Tree.extra_lbits[code];
+						if (extra !== 0) {
+							lc -= Tree.base_length[code];
+							send_bits(lc, extra); // send the extra length bits
+						}
+						dist--; // dist is now the match distance - 1
+						code = Tree.d_code(dist);
+
+						send_code(code, dtree); // send the distance code
+						extra = Tree.extra_dbits[code];
+						if (extra !== 0) {
+							dist -= Tree.base_dist[code];
+							send_bits(dist, extra); // send the extra distance bits
+						}
+					} // literal or match pair ?
+
+					// Check that the overlay between pending_buf and d_buf+l_buf is
+					// ok:
+				} while (lx < last_lit);
+			}
+
+			send_code(END_BLOCK, ltree);
+			last_eob_len = ltree[END_BLOCK * 2 + 1];
+		}
+
+		// Flush the bit buffer and align the output on a byte boundary
+		function bi_windup() {
+			if (bi_valid > 8) {
+				put_short(bi_buf);
+			} else if (bi_valid > 0) {
+				put_byte(bi_buf & 0xff);
+			}
+			bi_buf = 0;
+			bi_valid = 0;
+		}
+
+		// Copy a stored block, storing first the length and its
+		// one's complement if requested.
+		function copy_block(buf, // the input data
+		len, // its length
+		header // true if block header must be written
+		) {
+			bi_windup(); // align on byte boundary
+			last_eob_len = 8; // enough lookahead for inflate
+
+			if (header) {
+				put_short(len);
+				put_short(~len);
+			}
+
+			that.pending_buf.set(window.subarray(buf, buf + len), that.pending);
+			that.pending += len;
+		}
+
+		// Send a stored block
+		function _tr_stored_block(buf, // input block
+		stored_len, // length of input block
+		eof // true if this is the last block for a file
+		) {
+			send_bits((STORED_BLOCK << 1) + (eof ? 1 : 0), 3); // send block type
+			copy_block(buf, stored_len, true); // with header
+		}
+
+		// Determine the best encoding for the current block: dynamic trees, static
+		// trees or store, and output the encoded block to the zip file.
+		function _tr_flush_block(buf, // input block, or NULL if too old
+		stored_len, // length of input block
+		eof // true if this is the last block for a file
+		) {
+			var opt_lenb, static_lenb;// opt_len and static_len in bytes
+			var max_blindex = 0; // index of last bit length code of non zero freq
+
+			// Build the Huffman trees unless a stored block is forced
+			if (level > 0) {
+				// Construct the literal and distance trees
+				l_desc.build_tree(that);
+
+				d_desc.build_tree(that);
+
+				// At this point, opt_len and static_len are the total bit lengths
+				// of
+				// the compressed block data, excluding the tree representations.
+
+				// Build the bit length tree for the above two trees, and get the
+				// index
+				// in bl_order of the last bit length code to send.
+				max_blindex = build_bl_tree();
+
+				// Determine the best encoding. Compute first the block length in
+				// bytes
+				opt_lenb = (that.opt_len + 3 + 7) >>> 3;
+				static_lenb = (that.static_len + 3 + 7) >>> 3;
+
+				if (static_lenb <= opt_lenb)
+					opt_lenb = static_lenb;
+			} else {
+				opt_lenb = static_lenb = stored_len + 5; // force a stored block
+			}
+
+			if ((stored_len + 4 <= opt_lenb) && buf != -1) {
+				// 4: two words for the lengths
+				// The test buf != NULL is only necessary if LIT_BUFSIZE > WSIZE.
+				// Otherwise we can't have processed more than WSIZE input bytes
+				// since
+				// the last block flush, because compression would have been
+				// successful. If LIT_BUFSIZE <= WSIZE, it is never too late to
+				// transform a block into a stored block.
+				_tr_stored_block(buf, stored_len, eof);
+			} else if (static_lenb == opt_lenb) {
+				send_bits((STATIC_TREES << 1) + (eof ? 1 : 0), 3);
+				compress_block(StaticTree.static_ltree, StaticTree.static_dtree);
+			} else {
+				send_bits((DYN_TREES << 1) + (eof ? 1 : 0), 3);
+				send_all_trees(l_desc.max_code + 1, d_desc.max_code + 1, max_blindex + 1);
+				compress_block(dyn_ltree, dyn_dtree);
+			}
+
+			// The above check is made mod 2^32, for files larger than 512 MB
+			// and uLong implemented on 32 bits.
+
+			init_block();
+
+			if (eof) {
+				bi_windup();
+			}
+		}
+
+		function flush_block_only(eof) {
+			_tr_flush_block(block_start >= 0 ? block_start : -1, strstart - block_start, eof);
+			block_start = strstart;
+			strm.flush_pending();
+		}
+
+		// Fill the window when the lookahead becomes insufficient.
+		// Updates strstart and lookahead.
+		//
+		// IN assertion: lookahead < MIN_LOOKAHEAD
+		// OUT assertions: strstart <= window_size-MIN_LOOKAHEAD
+		// At least one byte has been read, or avail_in === 0; reads are
+		// performed for at least two bytes (required for the zip translate_eol
+		// option -- not supported here).
+		function fill_window() {
+			var n, m;
+			var p;
+			var more; // Amount of free space at the end of the window.
+
+			do {
+				more = (window_size - lookahead - strstart);
+
+				// Deal with !@#$% 64K limit:
+				if (more === 0 && strstart === 0 && lookahead === 0) {
+					more = w_size;
+				} else if (more == -1) {
+					// Very unlikely, but possible on 16 bit machine if strstart ==
+					// 0
+					// and lookahead == 1 (input done one byte at time)
+					more--;
+
+					// If the window is almost full and there is insufficient
+					// lookahead,
+					// move the upper half to the lower one to make room in the
+					// upper half.
+				} else if (strstart >= w_size + w_size - MIN_LOOKAHEAD) {
+					window.set(window.subarray(w_size, w_size + w_size), 0);
+
+					match_start -= w_size;
+					strstart -= w_size; // we now have strstart >= MAX_DIST
+					block_start -= w_size;
+
+					// Slide the hash table (could be avoided with 32 bit values
+					// at the expense of memory usage). We slide even when level ==
+					// 0
+					// to keep the hash table consistent if we switch back to level
+					// > 0
+					// later. (Using level 0 permanently is not an optimal usage of
+					// zlib, so we don't care about this pathological case.)
+
+					n = hash_size;
+					p = n;
+					do {
+						m = (head[--p] & 0xffff);
+						head[p] = (m >= w_size ? m - w_size : 0);
+					} while (--n !== 0);
+
+					n = w_size;
+					p = n;
+					do {
+						m = (prev[--p] & 0xffff);
+						prev[p] = (m >= w_size ? m - w_size : 0);
+						// If n is not on any hash chain, prev[n] is garbage but
+						// its value will never be used.
+					} while (--n !== 0);
+					more += w_size;
+				}
+
+				if (strm.avail_in === 0)
+					return;
+
+				// If there was no sliding:
+				// strstart <= WSIZE+MAX_DIST-1 && lookahead <= MIN_LOOKAHEAD - 1 &&
+				// more == window_size - lookahead - strstart
+				// => more >= window_size - (MIN_LOOKAHEAD-1 + WSIZE + MAX_DIST-1)
+				// => more >= window_size - 2*WSIZE + 2
+				// In the BIG_MEM or MMAP case (not yet supported),
+				// window_size == input_size + MIN_LOOKAHEAD &&
+				// strstart + s->lookahead <= input_size => more >= MIN_LOOKAHEAD.
+				// Otherwise, window_size == 2*WSIZE so more >= 2.
+				// If there was sliding, more >= WSIZE. So in all cases, more >= 2.
+
+				n = strm.read_buf(window, strstart + lookahead, more);
+				lookahead += n;
+
+				// Initialize the hash value now that we have some input:
+				if (lookahead >= MIN_MATCH) {
+					ins_h = window[strstart] & 0xff;
+					ins_h = (((ins_h) << hash_shift) ^ (window[strstart + 1] & 0xff)) & hash_mask;
+				}
+				// If the whole input has less than MIN_MATCH bytes, ins_h is
+				// garbage,
+				// but this is not important since only literal bytes will be
+				// emitted.
+			} while (lookahead < MIN_LOOKAHEAD && strm.avail_in !== 0);
+		}
+
+		// Copy without compression as much as possible from the input stream,
+		// return
+		// the current block state.
+		// This function does not insert new strings in the dictionary since
+		// uncompressible data is probably not useful. This function is used
+		// only for the level=0 compression option.
+		// NOTE: this function should be optimized to avoid extra copying from
+		// window to pending_buf.
+		function deflate_stored(flush) {
+			// Stored blocks are limited to 0xffff bytes, pending_buf is limited
+			// to pending_buf_size, and each stored block has a 5 byte header:
+
+			var max_block_size = 0xffff;
+			var max_start;
+
+			if (max_block_size > pending_buf_size - 5) {
+				max_block_size = pending_buf_size - 5;
+			}
+
+			// Copy as much as possible from input to output:
+			while (true) {
+				// Fill the window as much as possible:
+				if (lookahead <= 1) {
+					fill_window();
+					if (lookahead === 0 && flush == Z_NO_FLUSH)
+						return NeedMore;
+					if (lookahead === 0)
+						break; // flush the current block
+				}
+
+				strstart += lookahead;
+				lookahead = 0;
+
+				// Emit a stored block if pending_buf will be full:
+				max_start = block_start + max_block_size;
+				if (strstart === 0 || strstart >= max_start) {
+					// strstart === 0 is possible when wraparound on 16-bit machine
+					lookahead = (strstart - max_start);
+					strstart = max_start;
+
+					flush_block_only(false);
+					if (strm.avail_out === 0)
+						return NeedMore;
+
+				}
+
+				// Flush if we may have to slide, otherwise block_start may become
+				// negative and the data will be gone:
+				if (strstart - block_start >= w_size - MIN_LOOKAHEAD) {
+					flush_block_only(false);
+					if (strm.avail_out === 0)
+						return NeedMore;
+				}
+			}
+
+			flush_block_only(flush == Z_FINISH);
+			if (strm.avail_out === 0)
+				return (flush == Z_FINISH) ? FinishStarted : NeedMore;
+
+			return flush == Z_FINISH ? FinishDone : BlockDone;
+		}
+
+		function longest_match(cur_match) {
+			var chain_length = max_chain_length; // max hash chain length
+			var scan = strstart; // current string
+			var match; // matched string
+			var len; // length of current match
+			var best_len = prev_length; // best match length so far
+			var limit = strstart > (w_size - MIN_LOOKAHEAD) ? strstart - (w_size - MIN_LOOKAHEAD) : 0;
+			var _nice_match = nice_match;
+
+			// Stop when cur_match becomes <= limit. To simplify the code,
+			// we prevent matches with the string of window index 0.
+
+			var wmask = w_mask;
+
+			var strend = strstart + MAX_MATCH;
+			var scan_end1 = window[scan + best_len - 1];
+			var scan_end = window[scan + best_len];
+
+			// The code is optimized for HASH_BITS >= 8 and MAX_MATCH-2 multiple of
+			// 16.
+			// It is easy to get rid of this optimization if necessary.
+
+			// Do not waste too much time if we already have a good match:
+			if (prev_length >= good_match) {
+				chain_length >>= 2;
+			}
+
+			// Do not look for matches beyond the end of the input. This is
+			// necessary
+			// to make deflate deterministic.
+			if (_nice_match > lookahead)
+				_nice_match = lookahead;
+
+			do {
+				match = cur_match;
+
+				// Skip to next match if the match length cannot increase
+				// or if the match length is less than 2:
+				if (window[match + best_len] != scan_end || window[match + best_len - 1] != scan_end1 || window[match] != window[scan]
+						|| window[++match] != window[scan + 1])
+					continue;
+
+				// The check at best_len-1 can be removed because it will be made
+				// again later. (This heuristic is not always a win.)
+				// It is not necessary to compare scan[2] and match[2] since they
+				// are always equal when the other bytes match, given that
+				// the hash keys are equal and that HASH_BITS >= 8.
+				scan += 2;
+				match++;
+
+				// We check for insufficient lookahead only every 8th comparison;
+				// the 256th check will be made at strstart+258.
+				do {
+				} while (window[++scan] == window[++match] && window[++scan] == window[++match] && window[++scan] == window[++match]
+						&& window[++scan] == window[++match] && window[++scan] == window[++match] && window[++scan] == window[++match]
+						&& window[++scan] == window[++match] && window[++scan] == window[++match] && scan < strend);
+
+				len = MAX_MATCH - (strend - scan);
+				scan = strend - MAX_MATCH;
+
+				if (len > best_len) {
+					match_start = cur_match;
+					best_len = len;
+					if (len >= _nice_match)
+						break;
+					scan_end1 = window[scan + best_len - 1];
+					scan_end = window[scan + best_len];
+				}
+
+			} while ((cur_match = (prev[cur_match & wmask] & 0xffff)) > limit && --chain_length !== 0);
+
+			if (best_len <= lookahead)
+				return best_len;
+			return lookahead;
+		}
+
+		// Compress as much as possible from the input stream, return the current
+		// block state.
+		// This function does not perform lazy evaluation of matches and inserts
+		// new strings in the dictionary only for unmatched strings or for short
+		// matches. It is used only for the fast compression options.
+		function deflate_fast(flush) {
+			// short hash_head = 0; // head of the hash chain
+			var hash_head = 0; // head of the hash chain
+			var bflush; // set if current block must be flushed
+
+			while (true) {
+				// Make sure that we always have enough lookahead, except
+				// at the end of the input file. We need MAX_MATCH bytes
+				// for the next match, plus MIN_MATCH bytes to insert the
+				// string following the next match.
+				if (lookahead < MIN_LOOKAHEAD) {
+					fill_window();
+					if (lookahead < MIN_LOOKAHEAD && flush == Z_NO_FLUSH) {
+						return NeedMore;
+					}
+					if (lookahead === 0)
+						break; // flush the current block
+				}
+
+				// Insert the string window[strstart .. strstart+2] in the
+				// dictionary, and set hash_head to the head of the hash chain:
+				if (lookahead >= MIN_MATCH) {
+					ins_h = (((ins_h) << hash_shift) ^ (window[(strstart) + (MIN_MATCH - 1)] & 0xff)) & hash_mask;
+
+					// prev[strstart&w_mask]=hash_head=head[ins_h];
+					hash_head = (head[ins_h] & 0xffff);
+					prev[strstart & w_mask] = head[ins_h];
+					head[ins_h] = strstart;
+				}
+
+				// Find the longest match, discarding those <= prev_length.
+				// At this point we have always match_length < MIN_MATCH
+
+				if (hash_head !== 0 && ((strstart - hash_head) & 0xffff) <= w_size - MIN_LOOKAHEAD) {
+					// To simplify the code, we prevent matches with the string
+					// of window index 0 (in particular we have to avoid a match
+					// of the string with itself at the start of the input file).
+					if (strategy != Z_HUFFMAN_ONLY) {
+						match_length = longest_match(hash_head);
+					}
+					// longest_match() sets match_start
+				}
+				if (match_length >= MIN_MATCH) {
+					// check_match(strstart, match_start, match_length);
+
+					bflush = _tr_tally(strstart - match_start, match_length - MIN_MATCH);
+
+					lookahead -= match_length;
+
+					// Insert new strings in the hash table only if the match length
+					// is not too large. This saves time but degrades compression.
+					if (match_length <= max_lazy_match && lookahead >= MIN_MATCH) {
+						match_length--; // string at strstart already in hash table
+						do {
+							strstart++;
+
+							ins_h = ((ins_h << hash_shift) ^ (window[(strstart) + (MIN_MATCH - 1)] & 0xff)) & hash_mask;
+							// prev[strstart&w_mask]=hash_head=head[ins_h];
+							hash_head = (head[ins_h] & 0xffff);
+							prev[strstart & w_mask] = head[ins_h];
+							head[ins_h] = strstart;
+
+							// strstart never exceeds WSIZE-MAX_MATCH, so there are
+							// always MIN_MATCH bytes ahead.
+						} while (--match_length !== 0);
+						strstart++;
+					} else {
+						strstart += match_length;
+						match_length = 0;
+						ins_h = window[strstart] & 0xff;
+
+						ins_h = (((ins_h) << hash_shift) ^ (window[strstart + 1] & 0xff)) & hash_mask;
+						// If lookahead < MIN_MATCH, ins_h is garbage, but it does
+						// not
+						// matter since it will be recomputed at next deflate call.
+					}
+				} else {
+					// No match, output a literal byte
+
+					bflush = _tr_tally(0, window[strstart] & 0xff);
+					lookahead--;
+					strstart++;
+				}
+				if (bflush) {
+
+					flush_block_only(false);
+					if (strm.avail_out === 0)
+						return NeedMore;
+				}
+			}
+
+			flush_block_only(flush == Z_FINISH);
+			if (strm.avail_out === 0) {
+				if (flush == Z_FINISH)
+					return FinishStarted;
+				else
+					return NeedMore;
+			}
+			return flush == Z_FINISH ? FinishDone : BlockDone;
+		}
+
+		// Same as above, but achieves better compression. We use a lazy
+		// evaluation for matches: a match is finally adopted only if there is
+		// no better match at the next window position.
+		function deflate_slow(flush) {
+			// short hash_head = 0; // head of hash chain
+			var hash_head = 0; // head of hash chain
+			var bflush; // set if current block must be flushed
+			var max_insert;
+
+			// Process the input block.
+			while (true) {
+				// Make sure that we always have enough lookahead, except
+				// at the end of the input file. We need MAX_MATCH bytes
+				// for the next match, plus MIN_MATCH bytes to insert the
+				// string following the next match.
+
+				if (lookahead < MIN_LOOKAHEAD) {
+					fill_window();
+					if (lookahead < MIN_LOOKAHEAD && flush == Z_NO_FLUSH) {
+						return NeedMore;
+					}
+					if (lookahead === 0)
+						break; // flush the current block
+				}
+
+				// Insert the string window[strstart .. strstart+2] in the
+				// dictionary, and set hash_head to the head of the hash chain:
+
+				if (lookahead >= MIN_MATCH) {
+					ins_h = (((ins_h) << hash_shift) ^ (window[(strstart) + (MIN_MATCH - 1)] & 0xff)) & hash_mask;
+					// prev[strstart&w_mask]=hash_head=head[ins_h];
+					hash_head = (head[ins_h] & 0xffff);
+					prev[strstart & w_mask] = head[ins_h];
+					head[ins_h] = strstart;
+				}
+
+				// Find the longest match, discarding those <= prev_length.
+				prev_length = match_length;
+				prev_match = match_start;
+				match_length = MIN_MATCH - 1;
+
+				if (hash_head !== 0 && prev_length < max_lazy_match && ((strstart - hash_head) & 0xffff) <= w_size - MIN_LOOKAHEAD) {
+					// To simplify the code, we prevent matches with the string
+					// of window index 0 (in particular we have to avoid a match
+					// of the string with itself at the start of the input file).
+
+					if (strategy != Z_HUFFMAN_ONLY) {
+						match_length = longest_match(hash_head);
+					}
+					// longest_match() sets match_start
+
+					if (match_length <= 5 && (strategy == Z_FILTERED || (match_length == MIN_MATCH && strstart - match_start > 4096))) {
+
+						// If prev_match is also MIN_MATCH, match_start is garbage
+						// but we will ignore the current match anyway.
+						match_length = MIN_MATCH - 1;
+					}
+				}
+
+				// If there was a match at the previous step and the current
+				// match is not better, output the previous match:
+				if (prev_length >= MIN_MATCH && match_length <= prev_length) {
+					max_insert = strstart + lookahead - MIN_MATCH;
+					// Do not insert strings in hash table beyond this.
+
+					// check_match(strstart-1, prev_match, prev_length);
+
+					bflush = _tr_tally(strstart - 1 - prev_match, prev_length - MIN_MATCH);
+
+					// Insert in hash table all strings up to the end of the match.
+					// strstart-1 and strstart are already inserted. If there is not
+					// enough lookahead, the last two strings are not inserted in
+					// the hash table.
+					lookahead -= prev_length - 1;
+					prev_length -= 2;
+					do {
+						if (++strstart <= max_insert) {
+							ins_h = (((ins_h) << hash_shift) ^ (window[(strstart) + (MIN_MATCH - 1)] & 0xff)) & hash_mask;
+							// prev[strstart&w_mask]=hash_head=head[ins_h];
+							hash_head = (head[ins_h] & 0xffff);
+							prev[strstart & w_mask] = head[ins_h];
+							head[ins_h] = strstart;
+						}
+					} while (--prev_length !== 0);
+					match_available = 0;
+					match_length = MIN_MATCH - 1;
+					strstart++;
+
+					if (bflush) {
+						flush_block_only(false);
+						if (strm.avail_out === 0)
+							return NeedMore;
+					}
+				} else if (match_available !== 0) {
+
+					// If there was no match at the previous position, output a
+					// single literal. If there was a match but the current match
+					// is longer, truncate the previous match to a single literal.
+
+					bflush = _tr_tally(0, window[strstart - 1] & 0xff);
+
+					if (bflush) {
+						flush_block_only(false);
+					}
+					strstart++;
+					lookahead--;
+					if (strm.avail_out === 0)
+						return NeedMore;
+				} else {
+					// There is no previous match to compare with, wait for
+					// the next step to decide.
+
+					match_available = 1;
+					strstart++;
+					lookahead--;
+				}
+			}
+
+			if (match_available !== 0) {
+				bflush = _tr_tally(0, window[strstart - 1] & 0xff);
+				match_available = 0;
+			}
+			flush_block_only(flush == Z_FINISH);
+
+			if (strm.avail_out === 0) {
+				if (flush == Z_FINISH)
+					return FinishStarted;
+				else
+					return NeedMore;
+			}
+
+			return flush == Z_FINISH ? FinishDone : BlockDone;
+		}
+
+		function deflateReset(strm) {
+			strm.total_in = strm.total_out = 0;
+			strm.msg = null; //
+			
+			that.pending = 0;
+			that.pending_out = 0;
+
+			status = BUSY_STATE;
+
+			last_flush = Z_NO_FLUSH;
+
+			tr_init();
+			lm_init();
+			return Z_OK;
+		}
+
+		that.deflateInit = function(strm, _level, bits, _method, memLevel, _strategy) {
+			if (!_method)
+				_method = Z_DEFLATED;
+			if (!memLevel)
+				memLevel = DEF_MEM_LEVEL;
+			if (!_strategy)
+				_strategy = Z_DEFAULT_STRATEGY;
+
+			// byte[] my_version=ZLIB_VERSION;
+
+			//
+			// if (!version || version[0] != my_version[0]
+			// || stream_size != sizeof(z_stream)) {
+			// return Z_VERSION_ERROR;
+			// }
+
+			strm.msg = null;
+
+			if (_level == Z_DEFAULT_COMPRESSION)
+				_level = 6;
+
+			if (memLevel < 1 || memLevel > MAX_MEM_LEVEL || _method != Z_DEFLATED || bits < 9 || bits > 15 || _level < 0 || _level > 9 || _strategy < 0
+					|| _strategy > Z_HUFFMAN_ONLY) {
+				return Z_STREAM_ERROR;
+			}
+
+			strm.dstate = that;
+
+			w_bits = bits;
+			w_size = 1 << w_bits;
+			w_mask = w_size - 1;
+
+			hash_bits = memLevel + 7;
+			hash_size = 1 << hash_bits;
+			hash_mask = hash_size - 1;
+			hash_shift = Math.floor((hash_bits + MIN_MATCH - 1) / MIN_MATCH);
+
+			window = new Uint8Array(w_size * 2);
+			prev = [];
+			head = [];
+
+			lit_bufsize = 1 << (memLevel + 6); // 16K elements by default
+
+			// We overlay pending_buf and d_buf+l_buf. This works since the average
+			// output size for (length,distance) codes is <= 24 bits.
+			that.pending_buf = new Uint8Array(lit_bufsize * 4);
+			pending_buf_size = lit_bufsize * 4;
+
+			d_buf = Math.floor(lit_bufsize / 2);
+			l_buf = (1 + 2) * lit_bufsize;
+
+			level = _level;
+
+			strategy = _strategy;
+			method = _method & 0xff;
+
+			return deflateReset(strm);
+		};
+
+		that.deflateEnd = function() {
+			if (status != INIT_STATE && status != BUSY_STATE && status != FINISH_STATE) {
+				return Z_STREAM_ERROR;
+			}
+			// Deallocate in reverse order of allocations:
+			that.pending_buf = null;
+			head = null;
+			prev = null;
+			window = null;
+			// free
+			that.dstate = null;
+			return status == BUSY_STATE ? Z_DATA_ERROR : Z_OK;
+		};
+
+		that.deflateParams = function(strm, _level, _strategy) {
+			var err = Z_OK;
+
+			if (_level == Z_DEFAULT_COMPRESSION) {
+				_level = 6;
+			}
+			if (_level < 0 || _level > 9 || _strategy < 0 || _strategy > Z_HUFFMAN_ONLY) {
+				return Z_STREAM_ERROR;
+			}
+
+			if (config_table[level].func != config_table[_level].func && strm.total_in !== 0) {
+				// Flush the last buffer:
+				err = strm.deflate(Z_PARTIAL_FLUSH);
+			}
+
+			if (level != _level) {
+				level = _level;
+				max_lazy_match = config_table[level].max_lazy;
+				good_match = config_table[level].good_length;
+				nice_match = config_table[level].nice_length;
+				max_chain_length = config_table[level].max_chain;
+			}
+			strategy = _strategy;
+			return err;
+		};
+
+		that.deflateSetDictionary = function(strm, dictionary, dictLength) {
+			var length = dictLength;
+			var n, index = 0;
+
+			if (!dictionary || status != INIT_STATE)
+				return Z_STREAM_ERROR;
+
+			if (length < MIN_MATCH)
+				return Z_OK;
+			if (length > w_size - MIN_LOOKAHEAD) {
+				length = w_size - MIN_LOOKAHEAD;
+				index = dictLength - length; // use the tail of the dictionary
+			}
+			window.set(dictionary.subarray(index, index + length), 0);
+
+			strstart = length;
+			block_start = length;
+
+			// Insert all strings in the hash table (except for the last two bytes).
+			// s->lookahead stays null, so s->ins_h will be recomputed at the next
+			// call of fill_window.
+
+			ins_h = window[0] & 0xff;
+			ins_h = (((ins_h) << hash_shift) ^ (window[1] & 0xff)) & hash_mask;
+
+			for (n = 0; n <= length - MIN_MATCH; n++) {
+				ins_h = (((ins_h) << hash_shift) ^ (window[(n) + (MIN_MATCH - 1)] & 0xff)) & hash_mask;
+				prev[n & w_mask] = head[ins_h];
+				head[ins_h] = n;
+			}
+			return Z_OK;
+		};
+
+		that.deflate = function(_strm, flush) {
+			var i, header, level_flags, old_flush, bstate;
+
+			if (flush > Z_FINISH || flush < 0) {
+				return Z_STREAM_ERROR;
+			}
+
+			if (!_strm.next_out || (!_strm.next_in && _strm.avail_in !== 0) || (status == FINISH_STATE && flush != Z_FINISH)) {
+				_strm.msg = z_errmsg[Z_NEED_DICT - (Z_STREAM_ERROR)];
+				return Z_STREAM_ERROR;
+			}
+			if (_strm.avail_out === 0) {
+				_strm.msg = z_errmsg[Z_NEED_DICT - (Z_BUF_ERROR)];
+				return Z_BUF_ERROR;
+			}
+
+			strm = _strm; // just in case
+			old_flush = last_flush;
+			last_flush = flush;
+
+			// Write the zlib header
+			if (status == INIT_STATE) {
+				header = (Z_DEFLATED + ((w_bits - 8) << 4)) << 8;
+				level_flags = ((level - 1) & 0xff) >> 1;
+
+				if (level_flags > 3)
+					level_flags = 3;
+				header |= (level_flags << 6);
+				if (strstart !== 0)
+					header |= PRESET_DICT;
+				header += 31 - (header % 31);
+
+				status = BUSY_STATE;
+				putShortMSB(header);
+			}
+
+			// Flush as much pending output as possible
+			if (that.pending !== 0) {
+				strm.flush_pending();
+				if (strm.avail_out === 0) {
+					// console.log(" avail_out==0");
+					// Since avail_out is 0, deflate will be called again with
+					// more output space, but possibly with both pending and
+					// avail_in equal to zero. There won't be anything to do,
+					// but this is not an error situation so make sure we
+					// return OK instead of BUF_ERROR at next call of deflate:
+					last_flush = -1;
+					return Z_OK;
+				}
+
+				// Make sure there is something to do and avoid duplicate
+				// consecutive
+				// flushes. For repeated and useless calls with Z_FINISH, we keep
+				// returning Z_STREAM_END instead of Z_BUFF_ERROR.
+			} else if (strm.avail_in === 0 && flush <= old_flush && flush != Z_FINISH) {
+				strm.msg = z_errmsg[Z_NEED_DICT - (Z_BUF_ERROR)];
+				return Z_BUF_ERROR;
+			}
+
+			// User must not provide more input after the first FINISH:
+			if (status == FINISH_STATE && strm.avail_in !== 0) {
+				_strm.msg = z_errmsg[Z_NEED_DICT - (Z_BUF_ERROR)];
+				return Z_BUF_ERROR;
+			}
+
+			// Start a new block or continue the current one.
+			if (strm.avail_in !== 0 || lookahead !== 0 || (flush != Z_NO_FLUSH && status != FINISH_STATE)) {
+				bstate = -1;
+				switch (config_table[level].func) {
+				case STORED:
+					bstate = deflate_stored(flush);
+					break;
+				case FAST:
+					bstate = deflate_fast(flush);
+					break;
+				case SLOW:
+					bstate = deflate_slow(flush);
+					break;
+				default:
+				}
+
+				if (bstate == FinishStarted || bstate == FinishDone) {
+					status = FINISH_STATE;
+				}
+				if (bstate == NeedMore || bstate == FinishStarted) {
+					if (strm.avail_out === 0) {
+						last_flush = -1; // avoid BUF_ERROR next call, see above
+					}
+					return Z_OK;
+					// If flush != Z_NO_FLUSH && avail_out === 0, the next call
+					// of deflate should use the same flush parameter to make sure
+					// that the flush is complete. So we don't have to output an
+					// empty block here, this will be done at next call. This also
+					// ensures that for a very small output buffer, we emit at most
+					// one empty block.
+				}
+
+				if (bstate == BlockDone) {
+					if (flush == Z_PARTIAL_FLUSH) {
+						_tr_align();
+					} else { // FULL_FLUSH or SYNC_FLUSH
+						_tr_stored_block(0, 0, false);
+						// For a full flush, this empty block will be recognized
+						// as a special marker by inflate_sync().
+						if (flush == Z_FULL_FLUSH) {
+							// state.head[s.hash_size-1]=0;
+							for (i = 0; i < hash_size/*-1*/; i++)
+								// forget history
+								head[i] = 0;
+						}
+					}
+					strm.flush_pending();
+					if (strm.avail_out === 0) {
+						last_flush = -1; // avoid BUF_ERROR at next call, see above
+						return Z_OK;
+					}
+				}
+			}
+
+			if (flush != Z_FINISH)
+				return Z_OK;
+			return Z_STREAM_END;
+		};
+	}
+
+	// ZStream
+
+	function ZStream() {
+		var that = this;
+		that.next_in_index = 0;
+		that.next_out_index = 0;
+		// that.next_in; // next input byte
+		that.avail_in = 0; // number of bytes available at next_in
+		that.total_in = 0; // total nb of input bytes read so far
+		// that.next_out; // next output byte should be put there
+		that.avail_out = 0; // remaining free space at next_out
+		that.total_out = 0; // total nb of bytes output so far
+		// that.msg;
+		// that.dstate;
+	}
+
+	ZStream.prototype = {
+		deflateInit : function(level, bits) {
+			var that = this;
+			that.dstate = new Deflate();
+			if (!bits)
+				bits = MAX_BITS;
+			return that.dstate.deflateInit(that, level, bits);
+		},
+
+		deflate : function(flush) {
+			var that = this;
+			if (!that.dstate) {
+				return Z_STREAM_ERROR;
+			}
+			return that.dstate.deflate(that, flush);
+		},
+
+		deflateEnd : function() {
+			var that = this;
+			if (!that.dstate)
+				return Z_STREAM_ERROR;
+			var ret = that.dstate.deflateEnd();
+			that.dstate = null;
+			return ret;
+		},
+
+		deflateParams : function(level, strategy) {
+			var that = this;
+			if (!that.dstate)
+				return Z_STREAM_ERROR;
+			return that.dstate.deflateParams(that, level, strategy);
+		},
+
+		deflateSetDictionary : function(dictionary, dictLength) {
+			var that = this;
+			if (!that.dstate)
+				return Z_STREAM_ERROR;
+			return that.dstate.deflateSetDictionary(that, dictionary, dictLength);
+		},
+
+		// Read a new buffer from the current input stream, update the
+		// total number of bytes read. All deflate() input goes through
+		// this function so some applications may wish to modify it to avoid
+		// allocating a large strm->next_in buffer and copying from it.
+		// (See also flush_pending()).
+		read_buf : function(buf, start, size) {
+			var that = this;
+			var len = that.avail_in;
+			if (len > size)
+				len = size;
+			if (len === 0)
+				return 0;
+			that.avail_in -= len;
+			buf.set(that.next_in.subarray(that.next_in_index, that.next_in_index + len), start);
+			that.next_in_index += len;
+			that.total_in += len;
+			return len;
+		},
+
+		// Flush as much pending output as possible. All deflate() output goes
+		// through this function so some applications may wish to modify it
+		// to avoid allocating a large strm->next_out buffer and copying into it.
+		// (See also read_buf()).
+		flush_pending : function() {
+			var that = this;
+			var len = that.dstate.pending;
+
+			if (len > that.avail_out)
+				len = that.avail_out;
+			if (len === 0)
+				return;
+
+			// if (that.dstate.pending_buf.length <= that.dstate.pending_out || that.next_out.length <= that.next_out_index
+			// || that.dstate.pending_buf.length < (that.dstate.pending_out + len) || that.next_out.length < (that.next_out_index +
+			// len)) {
+			// console.log(that.dstate.pending_buf.length + ", " + that.dstate.pending_out + ", " + that.next_out.length + ", " +
+			// that.next_out_index + ", " + len);
+			// console.log("avail_out=" + that.avail_out);
+			// }
+
+			that.next_out.set(that.dstate.pending_buf.subarray(that.dstate.pending_out, that.dstate.pending_out + len), that.next_out_index);
+
+			that.next_out_index += len;
+			that.dstate.pending_out += len;
+			that.total_out += len;
+			that.avail_out -= len;
+			that.dstate.pending -= len;
+			if (that.dstate.pending === 0) {
+				that.dstate.pending_out = 0;
+			}
+		}
+	};
+
+	// Deflater
+
+	return function Deflater(level) {
+		var that = this;
+		var z = new ZStream();
+		var bufsize = 512;
+		var flush = Z_NO_FLUSH;
+		var buf = new Uint8Array(bufsize);
+
+		if (typeof level == "undefined")
+			level = Z_DEFAULT_COMPRESSION;
+		z.deflateInit(level);
+		z.next_out = buf;
+
+		that.append = function(data, onprogress) {
+			var err, buffers = [], lastIndex = 0, bufferIndex = 0, bufferSize = 0, array;
+			if (!data.length)
+				return;
+			z.next_in_index = 0;
+			z.next_in = data;
+			z.avail_in = data.length;
+			do {
+				z.next_out_index = 0;
+				z.avail_out = bufsize;
+				err = z.deflate(flush);
+				if (err != Z_OK)
+					throw "deflating: " + z.msg;
+				if (z.next_out_index)
+					if (z.next_out_index == bufsize)
+						buffers.push(new Uint8Array(buf));
+					else
+						buffers.push(new Uint8Array(buf.subarray(0, z.next_out_index)));
+				bufferSize += z.next_out_index;
+				if (onprogress && z.next_in_index > 0 && z.next_in_index != lastIndex) {
+					onprogress(z.next_in_index);
+					lastIndex = z.next_in_index;
+				}
+			} while (z.avail_in > 0 || z.avail_out === 0);
+			array = new Uint8Array(bufferSize);
+			buffers.forEach(function(chunk) {
+				array.set(chunk, bufferIndex);
+				bufferIndex += chunk.length;
+			});
+			return array;
+		};
+		that.flush = function() {
+			var err, buffers = [], bufferIndex = 0, bufferSize = 0, array;
+			do {
+				z.next_out_index = 0;
+				z.avail_out = bufsize;
+				err = z.deflate(Z_FINISH);
+				if (err != Z_STREAM_END && err != Z_OK)
+					throw "deflating: " + z.msg;
+				if (bufsize - z.avail_out > 0)
+					buffers.push(new Uint8Array(buf.subarray(0, z.next_out_index)));
+				bufferSize += z.next_out_index;
+			} while (z.avail_in > 0 || z.avail_out === 0);
+			z.deflateEnd();
+			array = new Uint8Array(bufferSize);
+			buffers.forEach(function(chunk) {
+				array.set(chunk, bufferIndex);
+				bufferIndex += chunk.length;
+			});
+			return array;
+		};
+	};
+})(this);
+// Generated by CoffeeScript 1.4.0
+
+/*
+# PNG.js
+# Copyright (c) 2011 Devon Govett
+# MIT LICENSE
+# 
+# Permission is hereby granted, free of charge, to any person obtaining a copy of this 
+# software and associated documentation files (the "Software"), to deal in the Software 
+# without restriction, including without limitation the rights to use, copy, modify, merge, 
+# publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons 
+# to whom the Software is furnished to do so, subject to the following conditions:
+# 
+# The above copyright notice and this permission notice shall be included in all copies or 
+# substantial portions of the Software.
+# 
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 
+# BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 
+# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+
+
+(function(global) {
+  var PNG;
+
+  PNG = (function() {
+    var APNG_BLEND_OP_OVER, APNG_BLEND_OP_SOURCE, APNG_DISPOSE_OP_BACKGROUND, APNG_DISPOSE_OP_NONE, APNG_DISPOSE_OP_PREVIOUS, makeImage, scratchCanvas, scratchCtx;
+
+    PNG.load = function(url, canvas, callback) {
+      var xhr,
+        _this = this;
+      if (typeof canvas === 'function') {
+        callback = canvas;
+      }
+      xhr = new XMLHttpRequest;
+      xhr.open("GET", url, true);
+      xhr.responseType = "arraybuffer";
+      xhr.onload = function() {
+        var data, png;
+        data = new Uint8Array(xhr.response || xhr.mozResponseArrayBuffer);
+        png = new PNG(data);
+        if (typeof (canvas != null ? canvas.getContext : void 0) === 'function') {
+          png.render(canvas);
+        }
+        return typeof callback === "function" ? callback(png) : void 0;
+      };
+      return xhr.send(null);
+    };
+
+    APNG_DISPOSE_OP_NONE = 0;
+
+    APNG_DISPOSE_OP_BACKGROUND = 1;
+
+    APNG_DISPOSE_OP_PREVIOUS = 2;
+
+    APNG_BLEND_OP_SOURCE = 0;
+
+    APNG_BLEND_OP_OVER = 1;
+
+    function PNG(data) {
+      var chunkSize, colors, palLen, delayDen, delayNum, frame, i, index, key, section, palShort, text, _i, _j, _ref;
+      this.data = data;
+      this.pos = 8;
+      this.palette = [];
+      this.imgData = [];
+      this.transparency = {};
+      this.animation = null;
+      this.text = {};
+      frame = null;
+      while (true) {
+        chunkSize = this.readUInt32();
+        section = ((function() {
+          var _i, _results;
+          _results = [];
+          for (i = _i = 0; _i < 4; i = ++_i) {
+            _results.push(String.fromCharCode(this.data[this.pos++]));
+          }
+          return _results;
+        }).call(this)).join('');
+        switch (section) {
+          case 'IHDR':
+            this.width = this.readUInt32();
+            this.height = this.readUInt32();
+            this.bits = this.data[this.pos++];
+            this.colorType = this.data[this.pos++];
+            this.compressionMethod = this.data[this.pos++];
+            this.filterMethod = this.data[this.pos++];
+            this.interlaceMethod = this.data[this.pos++];
+            break;
+          case 'acTL':
+            this.animation = {
+              numFrames: this.readUInt32(),
+              numPlays: this.readUInt32() || Infinity,
+              frames: []
+            };
+            break;
+          case 'PLTE':
+            this.palette = this.read(chunkSize);
+            break;
+          case 'fcTL':
+            if (frame) {
+              this.animation.frames.push(frame);
+            }
+            this.pos += 4;
+            frame = {
+              width: this.readUInt32(),
+              height: this.readUInt32(),
+              xOffset: this.readUInt32(),
+              yOffset: this.readUInt32()
+            };
+            delayNum = this.readUInt16();
+            delayDen = this.readUInt16() || 100;
+            frame.delay = 1000 * delayNum / delayDen;
+            frame.disposeOp = this.data[this.pos++];
+            frame.blendOp = this.data[this.pos++];
+            frame.data = [];
+            break;
+          case 'IDAT':
+          case 'fdAT':
+            if (section === 'fdAT') {
+              this.pos += 4;
+              chunkSize -= 4;
+            }
+            data = (frame != null ? frame.data : void 0) || this.imgData;
+            for (i = _i = 0; 0 <= chunkSize ? _i < chunkSize : _i > chunkSize; i = 0 <= chunkSize ? ++_i : --_i) {
+              data.push(this.data[this.pos++]);
+            }
+            break;
+          case 'tRNS':
+            this.transparency = {};
+            switch (this.colorType) {
+              case 3:
+            	palLen = this.palette.length/3;
+                this.transparency.indexed = this.read(chunkSize);
+                if(this.transparency.indexed.length > palLen)
+                	throw new Error('More transparent colors than palette size');
+                /*
+                 * According to the PNG spec trns should be increased to the same size as palette if shorter
+                 */
+                //palShort = 255 - this.transparency.indexed.length;
+                palShort = palLen - this.transparency.indexed.length;
+                if (palShort > 0) {
+                  for (i = _j = 0; 0 <= palShort ? _j < palShort : _j > palShort; i = 0 <= palShort ? ++_j : --_j) {
+                    this.transparency.indexed.push(255);
+                  }
+                }
+                break;
+              case 0:
+                this.transparency.grayscale = this.read(chunkSize)[0];
+                break;
+              case 2:
+                this.transparency.rgb = this.read(chunkSize);
+            }
+            break;
+          case 'tEXt':
+            text = this.read(chunkSize);
+            index = text.indexOf(0);
+            key = String.fromCharCode.apply(String, text.slice(0, index));
+            this.text[key] = String.fromCharCode.apply(String, text.slice(index + 1));
+            break;
+          case 'IEND':
+            if (frame) {
+              this.animation.frames.push(frame);
+            }
+            this.colors = (function() {
+              switch (this.colorType) {
+                case 0:
+                case 3:
+                case 4:
+                  return 1;
+                case 2:
+                case 6:
+                  return 3;
+              }
+            }).call(this);
+            this.hasAlphaChannel = (_ref = this.colorType) === 4 || _ref === 6;
+            colors = this.colors + (this.hasAlphaChannel ? 1 : 0);
+            this.pixelBitlength = this.bits * colors;
+            this.colorSpace = (function() {
+              switch (this.colors) {
+                case 1:
+                  return 'DeviceGray';
+                case 3:
+                  return 'DeviceRGB';
+              }
+            }).call(this);
+            this.imgData = new Uint8Array(this.imgData);
+            return;
+          default:
+            this.pos += chunkSize;
+        }
+        this.pos += 4;
+        if (this.pos > this.data.length) {
+          throw new Error("Incomplete or corrupt PNG file");
+        }
+      }
+      return;
+    }
+
+    PNG.prototype.read = function(bytes) {
+      var i, _i, _results;
+      _results = [];
+      for (i = _i = 0; 0 <= bytes ? _i < bytes : _i > bytes; i = 0 <= bytes ? ++_i : --_i) {
+        _results.push(this.data[this.pos++]);
+      }
+      return _results;
+    };
+
+    PNG.prototype.readUInt32 = function() {
+      var b1, b2, b3, b4;
+      b1 = this.data[this.pos++] << 24;
+      b2 = this.data[this.pos++] << 16;
+      b3 = this.data[this.pos++] << 8;
+      b4 = this.data[this.pos++];
+      return b1 | b2 | b3 | b4;
+    };
+
+    PNG.prototype.readUInt16 = function() {
+      var b1, b2;
+      b1 = this.data[this.pos++] << 8;
+      b2 = this.data[this.pos++];
+      return b1 | b2;
+    };
+
+    PNG.prototype.decodePixels = function(data) {
+      var abyte, c, col, i, left, length, p, pa, paeth, pb, pc, pixelBytes, pixels, pos, row, scanlineLength, upper, upperLeft, _i, _j, _k, _l, _m;
+      if (data == null) {
+        data = this.imgData;
+      }
+      if (data.length === 0) {
+        return new Uint8Array(0);
+      }
+      data = new FlateStream(data);
+      data = data.getBytes();
+      pixelBytes = this.pixelBitlength / 8;
+      scanlineLength = pixelBytes * this.width;
+      pixels = new Uint8Array(scanlineLength * this.height);
+      length = data.length;
+      row = 0;
+      pos = 0;
+      c = 0;
+      while (pos < length) {
+        switch (data[pos++]) {
+          case 0:
+            for (i = _i = 0; _i < scanlineLength; i = _i += 1) {
+              pixels[c++] = data[pos++];
+            }
+            break;
+          case 1:
+            for (i = _j = 0; _j < scanlineLength; i = _j += 1) {
+              abyte = data[pos++];
+              left = i < pixelBytes ? 0 : pixels[c - pixelBytes];
+              pixels[c++] = (abyte + left) % 256;
+            }
+            break;
+          case 2:
+            for (i = _k = 0; _k < scanlineLength; i = _k += 1) {
+              abyte = data[pos++];
+              col = (i - (i % pixelBytes)) / pixelBytes;
+              upper = row && pixels[(row - 1) * scanlineLength + col * pixelBytes + (i % pixelBytes)];
+              pixels[c++] = (upper + abyte) % 256;
+            }
+            break;
+          case 3:
+            for (i = _l = 0; _l < scanlineLength; i = _l += 1) {
+              abyte = data[pos++];
+              col = (i - (i % pixelBytes)) / pixelBytes;
+              left = i < pixelBytes ? 0 : pixels[c - pixelBytes];
+              upper = row && pixels[(row - 1) * scanlineLength + col * pixelBytes + (i % pixelBytes)];
+              pixels[c++] = (abyte + Math.floor((left + upper) / 2)) % 256;
+            }
+            break;
+          case 4:
+            for (i = _m = 0; _m < scanlineLength; i = _m += 1) {
+              abyte = data[pos++];
+              col = (i - (i % pixelBytes)) / pixelBytes;
+              left = i < pixelBytes ? 0 : pixels[c - pixelBytes];
+              if (row === 0) {
+                upper = upperLeft = 0;
+              } else {
+                upper = pixels[(row - 1) * scanlineLength + col * pixelBytes + (i % pixelBytes)];
+                upperLeft = col && pixels[(row - 1) * scanlineLength + (col - 1) * pixelBytes + (i % pixelBytes)];
+              }
+              p = left + upper - upperLeft;
+              pa = Math.abs(p - left);
+              pb = Math.abs(p - upper);
+              pc = Math.abs(p - upperLeft);
+              if (pa <= pb && pa <= pc) {
+                paeth = left;
+              } else if (pb <= pc) {
+                paeth = upper;
+              } else {
+                paeth = upperLeft;
+              }
+              pixels[c++] = (abyte + paeth) % 256;
+            }
+            break;
+          default:
+            throw new Error("Invalid filter algorithm: " + data[pos - 1]);
+        }
+        row++;
+      }
+      return pixels;
+    };
+
+    PNG.prototype.decodePalette = function() {
+      var c, i, length, palette, pos, ret, transparency, _i, _ref, _ref1;
+      palette = this.palette;
+      transparency = this.transparency.indexed || [];
+      ret = new Uint8Array((transparency.length || 0) + palette.length);
+      pos = 0;
+      length = palette.length;
+      c = 0;
+      for (i = _i = 0, _ref = palette.length; _i < _ref; i = _i += 3) {
+        ret[pos++] = palette[i];
+        ret[pos++] = palette[i + 1];
+        ret[pos++] = palette[i + 2];
+        ret[pos++] = (_ref1 = transparency[c++]) != null ? _ref1 : 255;
+      }
+      return ret;
+    };
+
+    PNG.prototype.copyToImageData = function(imageData, pixels) {
+      var alpha, colors, data, i, input, j, k, length, palette, v, _ref;
+      colors = this.colors;
+      palette = null;
+      alpha = this.hasAlphaChannel;
+      if (this.palette.length) {
+        palette = (_ref = this._decodedPalette) != null ? _ref : this._decodedPalette = this.decodePalette();
+        colors = 4;
+        alpha = true;
+      }
+      data = imageData.data || imageData;
+      length = data.length;
+      input = palette || pixels;
+      i = j = 0;
+      if (colors === 1) {
+        while (i < length) {
+          k = palette ? pixels[i / 4] * 4 : j;
+          v = input[k++];
+          data[i++] = v;
+          data[i++] = v;
+          data[i++] = v;
+          data[i++] = alpha ? input[k++] : 255;
+          j = k;
+        }
+      } else {
+        while (i < length) {
+          k = palette ? pixels[i / 4] * 4 : j;
+          data[i++] = input[k++];
+          data[i++] = input[k++];
+          data[i++] = input[k++];
+          data[i++] = alpha ? input[k++] : 255;
+          j = k;
+        }
+      }
+    };
+
+    PNG.prototype.decode = function() {
+      var ret;
+      ret = new Uint8Array(this.width * this.height * 4);
+      this.copyToImageData(ret, this.decodePixels());
+      return ret;
+    };
+
+    try {
+        scratchCanvas = global.document.createElement('canvas');
+        scratchCtx = scratchCanvas.getContext('2d');
+    } catch(e) {
+        return -1;
+    }
+
+    makeImage = function(imageData) {
+      var img;
+      scratchCtx.width = imageData.width;
+      scratchCtx.height = imageData.height;
+      scratchCtx.clearRect(0, 0, imageData.width, imageData.height);
+      scratchCtx.putImageData(imageData, 0, 0);
+      img = new Image;
+      img.src = scratchCanvas.toDataURL();
+      return img;
+    };
+
+    PNG.prototype.decodeFrames = function(ctx) {
+      var frame, i, imageData, pixels, _i, _len, _ref, _results;
+      if (!this.animation) {
+        return;
+      }
+      _ref = this.animation.frames;
+      _results = [];
+      for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) {
+        frame = _ref[i];
+        imageData = ctx.createImageData(frame.width, frame.height);
+        pixels = this.decodePixels(new Uint8Array(frame.data));
+        this.copyToImageData(imageData, pixels);
+        frame.imageData = imageData;
+        _results.push(frame.image = makeImage(imageData));
+      }
+      return _results;
+    };
+
+    PNG.prototype.renderFrame = function(ctx, number) {
+      var frame, frames, prev;
+      frames = this.animation.frames;
+      frame = frames[number];
+      prev = frames[number - 1];
+      if (number === 0) {
+        ctx.clearRect(0, 0, this.width, this.height);
+      }
+      if ((prev != null ? prev.disposeOp : void 0) === APNG_DISPOSE_OP_BACKGROUND) {
+        ctx.clearRect(prev.xOffset, prev.yOffset, prev.width, prev.height);
+      } else if ((prev != null ? prev.disposeOp : void 0) === APNG_DISPOSE_OP_PREVIOUS) {
+        ctx.putImageData(prev.imageData, prev.xOffset, prev.yOffset);
+      }
+      if (frame.blendOp === APNG_BLEND_OP_SOURCE) {
+        ctx.clearRect(frame.xOffset, frame.yOffset, frame.width, frame.height);
+      }
+      return ctx.drawImage(frame.image, frame.xOffset, frame.yOffset);
+    };
+
+    PNG.prototype.animate = function(ctx) {
+      var doFrame, frameNumber, frames, numFrames, numPlays, _ref,
+        _this = this;
+      frameNumber = 0;
+      _ref = this.animation, numFrames = _ref.numFrames, frames = _ref.frames, numPlays = _ref.numPlays;
+      return (doFrame = function() {
+        var f, frame;
+        f = frameNumber++ % numFrames;
+        frame = frames[f];
+        _this.renderFrame(ctx, f);
+        if (numFrames > 1 && frameNumber / numFrames < numPlays) {
+          return _this.animation._timeout = setTimeout(doFrame, frame.delay);
+        }
+      })();
+    };
+
+    PNG.prototype.stopAnimation = function() {
+      var _ref;
+      return clearTimeout((_ref = this.animation) != null ? _ref._timeout : void 0);
+    };
+
+    PNG.prototype.render = function(canvas) {
+      var ctx, data;
+      if (canvas._png) {
+        canvas._png.stopAnimation();
+      }
+      canvas._png = this;
+      canvas.width = this.width;
+      canvas.height = this.height;
+      ctx = canvas.getContext("2d");
+      if (this.animation) {
+        this.decodeFrames(ctx);
+        return this.animate(ctx);
+      } else {
+        data = ctx.createImageData(this.width, this.height);
+        this.copyToImageData(data, this.decodePixels());
+        return ctx.putImageData(data, 0, 0);
+      }
+    };
+
+    return PNG;
+
+  })();
+
+  global.PNG = PNG;
+
+})(typeof window !== "undefined" && window || this);
+/*
+ * Extracted from pdf.js
+ * https://github.com/andreasgal/pdf.js
+ *
+ * Copyright (c) 2011 Mozilla Foundation
+ *
+ * Contributors: Andreas Gal <gal@mozilla.com>
+ *               Chris G Jones <cjones@mozilla.com>
+ *               Shaon Barman <shaon.barman@gmail.com>
+ *               Vivien Nicolas <21@vingtetun.org>
+ *               Justin D'Arcangelo <justindarc@gmail.com>
+ *               Yury Delendik
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+var DecodeStream = (function() {
+  function constructor() {
+    this.pos = 0;
+    this.bufferLength = 0;
+    this.eof = false;
+    this.buffer = null;
+  }
+
+  constructor.prototype = {
+    ensureBuffer: function decodestream_ensureBuffer(requested) {
+      var buffer = this.buffer;
+      var current = buffer ? buffer.byteLength : 0;
+      if (requested < current)
+        return buffer;
+      var size = 512;
+      while (size < requested)
+        size <<= 1;
+      var buffer2 = new Uint8Array(size);
+      for (var i = 0; i < current; ++i)
+        buffer2[i] = buffer[i];
+      return this.buffer = buffer2;
+    },
+    getByte: function decodestream_getByte() {
+      var pos = this.pos;
+      while (this.bufferLength <= pos) {
+        if (this.eof)
+          return null;
+        this.readBlock();
+      }
+      return this.buffer[this.pos++];
+    },
+    getBytes: function decodestream_getBytes(length) {
+      var pos = this.pos;
+
+      if (length) {
+        this.ensureBuffer(pos + length);
+        var end = pos + length;
+
+        while (!this.eof && this.bufferLength < end)
+          this.readBlock();
+
+        var bufEnd = this.bufferLength;
+        if (end > bufEnd)
+          end = bufEnd;
+      } else {
+        while (!this.eof)
+          this.readBlock();
+
+        var end = this.bufferLength;
+      }
+
+      this.pos = end;
+      return this.buffer.subarray(pos, end);
+    },
+    lookChar: function decodestream_lookChar() {
+      var pos = this.pos;
+      while (this.bufferLength <= pos) {
+        if (this.eof)
+          return null;
+        this.readBlock();
+      }
+      return String.fromCharCode(this.buffer[this.pos]);
+    },
+    getChar: function decodestream_getChar() {
+      var pos = this.pos;
+      while (this.bufferLength <= pos) {
+        if (this.eof)
+          return null;
+        this.readBlock();
+      }
+      return String.fromCharCode(this.buffer[this.pos++]);
+    },
+    makeSubStream: function decodestream_makeSubstream(start, length, dict) {
+      var end = start + length;
+      while (this.bufferLength <= end && !this.eof)
+        this.readBlock();
+      return new Stream(this.buffer, start, length, dict);
+    },
+    skip: function decodestream_skip(n) {
+      if (!n)
+        n = 1;
+      this.pos += n;
+    },
+    reset: function decodestream_reset() {
+      this.pos = 0;
+    }
+  };
+
+  return constructor;
+})();
+
+var FlateStream = (function() {
+  if (typeof Uint32Array === 'undefined') {
+    return undefined;
+  }
+  var codeLenCodeMap = new Uint32Array([
+    16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15
+  ]);
+
+  var lengthDecode = new Uint32Array([
+    0x00003, 0x00004, 0x00005, 0x00006, 0x00007, 0x00008, 0x00009, 0x0000a,
+    0x1000b, 0x1000d, 0x1000f, 0x10011, 0x20013, 0x20017, 0x2001b, 0x2001f,
+    0x30023, 0x3002b, 0x30033, 0x3003b, 0x40043, 0x40053, 0x40063, 0x40073,
+    0x50083, 0x500a3, 0x500c3, 0x500e3, 0x00102, 0x00102, 0x00102
+  ]);
+
+  var distDecode = new Uint32Array([
+    0x00001, 0x00002, 0x00003, 0x00004, 0x10005, 0x10007, 0x20009, 0x2000d,
+    0x30011, 0x30019, 0x40021, 0x40031, 0x50041, 0x50061, 0x60081, 0x600c1,
+    0x70101, 0x70181, 0x80201, 0x80301, 0x90401, 0x90601, 0xa0801, 0xa0c01,
+    0xb1001, 0xb1801, 0xc2001, 0xc3001, 0xd4001, 0xd6001
+  ]);
+
+  var fixedLitCodeTab = [new Uint32Array([
+    0x70100, 0x80050, 0x80010, 0x80118, 0x70110, 0x80070, 0x80030, 0x900c0,
+    0x70108, 0x80060, 0x80020, 0x900a0, 0x80000, 0x80080, 0x80040, 0x900e0,
+    0x70104, 0x80058, 0x80018, 0x90090, 0x70114, 0x80078, 0x80038, 0x900d0,
+    0x7010c, 0x80068, 0x80028, 0x900b0, 0x80008, 0x80088, 0x80048, 0x900f0,
+    0x70102, 0x80054, 0x80014, 0x8011c, 0x70112, 0x80074, 0x80034, 0x900c8,
+    0x7010a, 0x80064, 0x80024, 0x900a8, 0x80004, 0x80084, 0x80044, 0x900e8,
+    0x70106, 0x8005c, 0x8001c, 0x90098, 0x70116, 0x8007c, 0x8003c, 0x900d8,
+    0x7010e, 0x8006c, 0x8002c, 0x900b8, 0x8000c, 0x8008c, 0x8004c, 0x900f8,
+    0x70101, 0x80052, 0x80012, 0x8011a, 0x70111, 0x80072, 0x80032, 0x900c4,
+    0x70109, 0x80062, 0x80022, 0x900a4, 0x80002, 0x80082, 0x80042, 0x900e4,
+    0x70105, 0x8005a, 0x8001a, 0x90094, 0x70115, 0x8007a, 0x8003a, 0x900d4,
+    0x7010d, 0x8006a, 0x8002a, 0x900b4, 0x8000a, 0x8008a, 0x8004a, 0x900f4,
+    0x70103, 0x80056, 0x80016, 0x8011e, 0x70113, 0x80076, 0x80036, 0x900cc,
+    0x7010b, 0x80066, 0x80026, 0x900ac, 0x80006, 0x80086, 0x80046, 0x900ec,
+    0x70107, 0x8005e, 0x8001e, 0x9009c, 0x70117, 0x8007e, 0x8003e, 0x900dc,
+    0x7010f, 0x8006e, 0x8002e, 0x900bc, 0x8000e, 0x8008e, 0x8004e, 0x900fc,
+    0x70100, 0x80051, 0x80011, 0x80119, 0x70110, 0x80071, 0x80031, 0x900c2,
+    0x70108, 0x80061, 0x80021, 0x900a2, 0x80001, 0x80081, 0x80041, 0x900e2,
+    0x70104, 0x80059, 0x80019, 0x90092, 0x70114, 0x80079, 0x80039, 0x900d2,
+    0x7010c, 0x80069, 0x80029, 0x900b2, 0x80009, 0x80089, 0x80049, 0x900f2,
+    0x70102, 0x80055, 0x80015, 0x8011d, 0x70112, 0x80075, 0x80035, 0x900ca,
+    0x7010a, 0x80065, 0x80025, 0x900aa, 0x80005, 0x80085, 0x80045, 0x900ea,
+    0x70106, 0x8005d, 0x8001d, 0x9009a, 0x70116, 0x8007d, 0x8003d, 0x900da,
+    0x7010e, 0x8006d, 0x8002d, 0x900ba, 0x8000d, 0x8008d, 0x8004d, 0x900fa,
+    0x70101, 0x80053, 0x80013, 0x8011b, 0x70111, 0x80073, 0x80033, 0x900c6,
+    0x70109, 0x80063, 0x80023, 0x900a6, 0x80003, 0x80083, 0x80043, 0x900e6,
+    0x70105, 0x8005b, 0x8001b, 0x90096, 0x70115, 0x8007b, 0x8003b, 0x900d6,
+    0x7010d, 0x8006b, 0x8002b, 0x900b6, 0x8000b, 0x8008b, 0x8004b, 0x900f6,
+    0x70103, 0x80057, 0x80017, 0x8011f, 0x70113, 0x80077, 0x80037, 0x900ce,
+    0x7010b, 0x80067, 0x80027, 0x900ae, 0x80007, 0x80087, 0x80047, 0x900ee,
+    0x70107, 0x8005f, 0x8001f, 0x9009e, 0x70117, 0x8007f, 0x8003f, 0x900de,
+    0x7010f, 0x8006f, 0x8002f, 0x900be, 0x8000f, 0x8008f, 0x8004f, 0x900fe,
+    0x70100, 0x80050, 0x80010, 0x80118, 0x70110, 0x80070, 0x80030, 0x900c1,
+    0x70108, 0x80060, 0x80020, 0x900a1, 0x80000, 0x80080, 0x80040, 0x900e1,
+    0x70104, 0x80058, 0x80018, 0x90091, 0x70114, 0x80078, 0x80038, 0x900d1,
+    0x7010c, 0x80068, 0x80028, 0x900b1, 0x80008, 0x80088, 0x80048, 0x900f1,
+    0x70102, 0x80054, 0x80014, 0x8011c, 0x70112, 0x80074, 0x80034, 0x900c9,
+    0x7010a, 0x80064, 0x80024, 0x900a9, 0x80004, 0x80084, 0x80044, 0x900e9,
+    0x70106, 0x8005c, 0x8001c, 0x90099, 0x70116, 0x8007c, 0x8003c, 0x900d9,
+    0x7010e, 0x8006c, 0x8002c, 0x900b9, 0x8000c, 0x8008c, 0x8004c, 0x900f9,
+    0x70101, 0x80052, 0x80012, 0x8011a, 0x70111, 0x80072, 0x80032, 0x900c5,
+    0x70109, 0x80062, 0x80022, 0x900a5, 0x80002, 0x80082, 0x80042, 0x900e5,
+    0x70105, 0x8005a, 0x8001a, 0x90095, 0x70115, 0x8007a, 0x8003a, 0x900d5,
+    0x7010d, 0x8006a, 0x8002a, 0x900b5, 0x8000a, 0x8008a, 0x8004a, 0x900f5,
+    0x70103, 0x80056, 0x80016, 0x8011e, 0x70113, 0x80076, 0x80036, 0x900cd,
+    0x7010b, 0x80066, 0x80026, 0x900ad, 0x80006, 0x80086, 0x80046, 0x900ed,
+    0x70107, 0x8005e, 0x8001e, 0x9009d, 0x70117, 0x8007e, 0x8003e, 0x900dd,
+    0x7010f, 0x8006e, 0x8002e, 0x900bd, 0x8000e, 0x8008e, 0x8004e, 0x900fd,
+    0x70100, 0x80051, 0x80011, 0x80119, 0x70110, 0x80071, 0x80031, 0x900c3,
+    0x70108, 0x80061, 0x80021, 0x900a3, 0x80001, 0x80081, 0x80041, 0x900e3,
+    0x70104, 0x80059, 0x80019, 0x90093, 0x70114, 0x80079, 0x80039, 0x900d3,
+    0x7010c, 0x80069, 0x80029, 0x900b3, 0x80009, 0x80089, 0x80049, 0x900f3,
+    0x70102, 0x80055, 0x80015, 0x8011d, 0x70112, 0x80075, 0x80035, 0x900cb,
+    0x7010a, 0x80065, 0x80025, 0x900ab, 0x80005, 0x80085, 0x80045, 0x900eb,
+    0x70106, 0x8005d, 0x8001d, 0x9009b, 0x70116, 0x8007d, 0x8003d, 0x900db,
+    0x7010e, 0x8006d, 0x8002d, 0x900bb, 0x8000d, 0x8008d, 0x8004d, 0x900fb,
+    0x70101, 0x80053, 0x80013, 0x8011b, 0x70111, 0x80073, 0x80033, 0x900c7,
+    0x70109, 0x80063, 0x80023, 0x900a7, 0x80003, 0x80083, 0x80043, 0x900e7,
+    0x70105, 0x8005b, 0x8001b, 0x90097, 0x70115, 0x8007b, 0x8003b, 0x900d7,
+    0x7010d, 0x8006b, 0x8002b, 0x900b7, 0x8000b, 0x8008b, 0x8004b, 0x900f7,
+    0x70103, 0x80057, 0x80017, 0x8011f, 0x70113, 0x80077, 0x80037, 0x900cf,
+    0x7010b, 0x80067, 0x80027, 0x900af, 0x80007, 0x80087, 0x80047, 0x900ef,
+    0x70107, 0x8005f, 0x8001f, 0x9009f, 0x70117, 0x8007f, 0x8003f, 0x900df,
+    0x7010f, 0x8006f, 0x8002f, 0x900bf, 0x8000f, 0x8008f, 0x8004f, 0x900ff
+  ]), 9];
+
+  var fixedDistCodeTab = [new Uint32Array([
+    0x50000, 0x50010, 0x50008, 0x50018, 0x50004, 0x50014, 0x5000c, 0x5001c,
+    0x50002, 0x50012, 0x5000a, 0x5001a, 0x50006, 0x50016, 0x5000e, 0x00000,
+    0x50001, 0x50011, 0x50009, 0x50019, 0x50005, 0x50015, 0x5000d, 0x5001d,
+    0x50003, 0x50013, 0x5000b, 0x5001b, 0x50007, 0x50017, 0x5000f, 0x00000
+  ]), 5];
+  
+  function error(e) {
+      throw new Error(e)
+  }
+
+  function constructor(bytes) {
+    //var bytes = stream.getBytes();
+    var bytesPos = 0;
+
+    var cmf = bytes[bytesPos++];
+    var flg = bytes[bytesPos++];
+    if (cmf == -1 || flg == -1)
+      error('Invalid header in flate stream');
+    if ((cmf & 0x0f) != 0x08)
+      error('Unknown compression method in flate stream');
+    if ((((cmf << 8) + flg) % 31) != 0)
+      error('Bad FCHECK in flate stream');
+    if (flg & 0x20)
+      error('FDICT bit set in flate stream');
+
+    this.bytes = bytes;
+    this.bytesPos = bytesPos;
+
+    this.codeSize = 0;
+    this.codeBuf = 0;
+
+    DecodeStream.call(this);
+  }
+
+  constructor.prototype = Object.create(DecodeStream.prototype);
+
+  constructor.prototype.getBits = function(bits) {
+    var codeSize = this.codeSize;
+    var codeBuf = this.codeBuf;
+    var bytes = this.bytes;
+    var bytesPos = this.bytesPos;
+
+    var b;
+    while (codeSize < bits) {
+      if (typeof (b = bytes[bytesPos++]) == 'undefined')
+        error('Bad encoding in flate stream');
+      codeBuf |= b << codeSize;
+      codeSize += 8;
+    }
+    b = codeBuf & ((1 << bits) - 1);
+    this.codeBuf = codeBuf >> bits;
+    this.codeSize = codeSize -= bits;
+    this.bytesPos = bytesPos;
+    return b;
+  };
+
+  constructor.prototype.getCode = function(table) {
+    var codes = table[0];
+    var maxLen = table[1];
+    var codeSize = this.codeSize;
+    var codeBuf = this.codeBuf;
+    var bytes = this.bytes;
+    var bytesPos = this.bytesPos;
+
+    while (codeSize < maxLen) {
+      var b;
+      if (typeof (b = bytes[bytesPos++]) == 'undefined')
+        error('Bad encoding in flate stream');
+      codeBuf |= (b << codeSize);
+      codeSize += 8;
+    }
+    var code = codes[codeBuf & ((1 << maxLen) - 1)];
+    var codeLen = code >> 16;
+    var codeVal = code & 0xffff;
+    if (codeSize == 0 || codeSize < codeLen || codeLen == 0)
+      error('Bad encoding in flate stream');
+    this.codeBuf = (codeBuf >> codeLen);
+    this.codeSize = (codeSize - codeLen);
+    this.bytesPos = bytesPos;
+    return codeVal;
+  };
+
+  constructor.prototype.generateHuffmanTable = function(lengths) {
+    var n = lengths.length;
+
+    // find max code length
+    var maxLen = 0;
+    for (var i = 0; i < n; ++i) {
+      if (lengths[i] > maxLen)
+        maxLen = lengths[i];
+    }
+
+    // build the table
+    var size = 1 << maxLen;
+    var codes = new Uint32Array(size);
+    for (var len = 1, code = 0, skip = 2;
+         len <= maxLen;
+         ++len, code <<= 1, skip <<= 1) {
+      for (var val = 0; val < n; ++val) {
+        if (lengths[val] == len) {
+          // bit-reverse the code
+          var code2 = 0;
+          var t = code;
+          for (var i = 0; i < len; ++i) {
+            code2 = (code2 << 1) | (t & 1);
+            t >>= 1;
+          }
+
+          // fill the table entries
+          for (var i = code2; i < size; i += skip)
+            codes[i] = (len << 16) | val;
+
+          ++code;
+        }
+      }
+    }
+
+    return [codes, maxLen];
+  };
+
+  constructor.prototype.readBlock = function() {
+    function repeat(stream, array, len, offset, what) {
+      var repeat = stream.getBits(len) + offset;
+      while (repeat-- > 0)
+        array[i++] = what;
+    }
+
+    // read block header
+    var hdr = this.getBits(3);
+    if (hdr & 1)
+      this.eof = true;
+    hdr >>= 1;
+
+    if (hdr == 0) { // uncompressed block
+      var bytes = this.bytes;
+      var bytesPos = this.bytesPos;
+      var b;
+
+      if (typeof (b = bytes[bytesPos++]) == 'undefined')
+        error('Bad block header in flate stream');
+      var blockLen = b;
+      if (typeof (b = bytes[bytesPos++]) == 'undefined')
+        error('Bad block header in flate stream');
+      blockLen |= (b << 8);
+      if (typeof (b = bytes[bytesPos++]) == 'undefined')
+        error('Bad block header in flate stream');
+      var check = b;
+      if (typeof (b = bytes[bytesPos++]) == 'undefined')
+        error('Bad block header in flate stream');
+      check |= (b << 8);
+      if (check != (~blockLen & 0xffff))
+        error('Bad uncompressed block length in flate stream');
+
+      this.codeBuf = 0;
+      this.codeSize = 0;
+
+      var bufferLength = this.bufferLength;
+      var buffer = this.ensureBuffer(bufferLength + blockLen);
+      var end = bufferLength + blockLen;
+      this.bufferLength = end;
+      for (var n = bufferLength; n < end; ++n) {
+        if (typeof (b = bytes[bytesPos++]) == 'undefined') {
+          this.eof = true;
+          break;
+        }
+        buffer[n] = b;
+      }
+      this.bytesPos = bytesPos;
+      return;
+    }
+
+    var litCodeTable;
+    var distCodeTable;
+    if (hdr == 1) { // compressed block, fixed codes
+      litCodeTable = fixedLitCodeTab;
+      distCodeTable = fixedDistCodeTab;
+    } else if (hdr == 2) { // compressed block, dynamic codes
+      var numLitCodes = this.getBits(5) + 257;
+      var numDistCodes = this.getBits(5) + 1;
+      var numCodeLenCodes = this.getBits(4) + 4;
+
+      // build the code lengths code table
+      var codeLenCodeLengths = Array(codeLenCodeMap.length);
+      var i = 0;
+      while (i < numCodeLenCodes)
+        codeLenCodeLengths[codeLenCodeMap[i++]] = this.getBits(3);
+      var codeLenCodeTab = this.generateHuffmanTable(codeLenCodeLengths);
+
+      // build the literal and distance code tables
+      var len = 0;
+      var i = 0;
+      var codes = numLitCodes + numDistCodes;
+      var codeLengths = new Array(codes);
+      while (i < codes) {
+        var code = this.getCode(codeLenCodeTab);
+        if (code == 16) {
+          repeat(this, codeLengths, 2, 3, len);
+        } else if (code == 17) {
+          repeat(this, codeLengths, 3, 3, len = 0);
+        } else if (code == 18) {
+          repeat(this, codeLengths, 7, 11, len = 0);
+        } else {
+          codeLengths[i++] = len = code;
+        }
+      }
+
+      litCodeTable =
+        this.generateHuffmanTable(codeLengths.slice(0, numLitCodes));
+      distCodeTable =
+        this.generateHuffmanTable(codeLengths.slice(numLitCodes, codes));
+    } else {
+      error('Unknown block type in flate stream');
+    }
+
+    var buffer = this.buffer;
+    var limit = buffer ? buffer.length : 0;
+    var pos = this.bufferLength;
+    while (true) {
+      var code1 = this.getCode(litCodeTable);
+      if (code1 < 256) {
+        if (pos + 1 >= limit) {
+          buffer = this.ensureBuffer(pos + 1);
+          limit = buffer.length;
+        }
+        buffer[pos++] = code1;
+        continue;
+      }
+      if (code1 == 256) {
+        this.bufferLength = pos;
+        return;
+      }
+      code1 -= 257;
+      code1 = lengthDecode[code1];
+      var code2 = code1 >> 16;
+      if (code2 > 0)
+        code2 = this.getBits(code2);
+      var len = (code1 & 0xffff) + code2;
+      code1 = this.getCode(distCodeTable);
+      code1 = distDecode[code1];
+      code2 = code1 >> 16;
+      if (code2 > 0)
+        code2 = this.getBits(code2);
+      var dist = (code1 & 0xffff) + code2;
+      if (pos + len >= limit) {
+        buffer = this.ensureBuffer(pos + len);
+        limit = buffer.length;
+      }
+      for (var k = 0; k < len; ++k, ++pos)
+        buffer[pos] = buffer[pos - dist];
+    }
+  };
+
+  return constructor;
+})();/**
+ * JavaScript Polyfill functions for jsPDF
+ * Collected from public resources by
+ * https://github.com/diegocr
+ */
+
+(function (global) {
+	var b64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
+
+	if (typeof global.btoa === 'undefined') {
+		global.btoa = function(data) {
+			//  discuss at: http://phpjs.org/functions/base64_encode/
+			// original by: Tyler Akins (http://rumkin.com)
+			// improved by: Bayron Guevara
+			// improved by: Thunder.m
+			// improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
+			// improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
+			// improved by: Rafal Kukawski (http://kukawski.pl)
+			// bugfixed by: Pellentesque Malesuada
+			//   example 1: base64_encode('Kevin van Zonneveld');
+			//   returns 1: 'S2V2aW4gdmFuIFpvbm5ldmVsZA=='
+
+			var o1,o2,o3,h1,h2,h3,h4,bits,i = 0,ac = 0,enc = '',tmp_arr = [];
+
+			if (!data) {
+				return data;
+			}
+
+			do { // pack three octets into four hexets
+				o1 = data.charCodeAt(i++);
+				o2 = data.charCodeAt(i++);
+				o3 = data.charCodeAt(i++);
+
+				bits = o1 << 16 | o2 << 8 | o3;
+
+				h1 = bits >> 18 & 0x3f;
+				h2 = bits >> 12 & 0x3f;
+				h3 = bits >> 6 & 0x3f;
+				h4 = bits & 0x3f;
+
+				// use hexets to index into b64, and append result to encoded string
+				tmp_arr[ac++] = b64.charAt(h1) + b64.charAt(h2) + b64.charAt(h3) + b64.charAt(h4);
+			} while (i < data.length);
+
+			enc = tmp_arr.join('');
+
+			var r = data.length % 3;
+
+			return (r ? enc.slice(0, r - 3) : enc) + '==='.slice(r || 3);
+		};
+	}
+
+	if (typeof global.atob === 'undefined') {
+		global.atob = function(data) {
+			//  discuss at: http://phpjs.org/functions/base64_decode/
+			// original by: Tyler Akins (http://rumkin.com)
+			// improved by: Thunder.m
+			// improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
+			// improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
+			//    input by: Aman Gupta
+			//    input by: Brett Zamir (http://brett-zamir.me)
+			// bugfixed by: Onno Marsman
+			// bugfixed by: Pellentesque Malesuada
+			// bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
+			//   example 1: base64_decode('S2V2aW4gdmFuIFpvbm5ldmVsZA==');
+			//   returns 1: 'Kevin van Zonneveld'
+
+			var o1,o2,o3,h1,h2,h3,h4,bits,i = 0,ac = 0,dec = '',tmp_arr = [];
+
+			if (!data) {
+				return data;
+			}
+
+			data += '';
+
+			do { // unpack four hexets into three octets using index points in b64
+				h1 = b64.indexOf(data.charAt(i++));
+				h2 = b64.indexOf(data.charAt(i++));
+				h3 = b64.indexOf(data.charAt(i++));
+				h4 = b64.indexOf(data.charAt(i++));
+
+				bits = h1 << 18 | h2 << 12 | h3 << 6 | h4;
+
+				o1 = bits >> 16 & 0xff;
+				o2 = bits >> 8 & 0xff;
+				o3 = bits & 0xff;
+
+				if (h3 == 64) {
+					tmp_arr[ac++] = String.fromCharCode(o1);
+				} else if (h4 == 64) {
+					tmp_arr[ac++] = String.fromCharCode(o1, o2);
+				} else {
+					tmp_arr[ac++] = String.fromCharCode(o1, o2, o3);
+				}
+			} while (i < data.length);
+
+			dec = tmp_arr.join('');
+
+			return dec;
+		};
+	}
+
+	if (!Array.prototype.map) {
+		Array.prototype.map = function(fun /*, thisArg */) {
+			if (this === void 0 || this === null || typeof fun !== "function")
+				throw new TypeError();
+
+			var t = Object(this), len = t.length >>> 0, res = new Array(len);
+			var thisArg = arguments.length > 1 ? arguments[1] : void 0;
+			for (var i = 0; i < len; i++) {
+				// NOTE: Absolute correctness would demand Object.defineProperty
+				//       be used.  But this method is fairly new, and failure is
+				//       possible only if Object.prototype or Array.prototype
+				//       has a property |i| (very unlikely), so use a less-correct
+				//       but more portable alternative.
+				if (i in t)
+					res[i] = fun.call(thisArg, t[i], i, t);
+			}
+
+			return res;
+		};
+	}
+
+
+	if(!Array.isArray) {
+		Array.isArray = function(arg) {
+			return Object.prototype.toString.call(arg) === '[object Array]';
+		};
+	}
+
+	if (!Array.prototype.forEach) {
+		Array.prototype.forEach = function(fun, thisArg) {
+			"use strict";
+
+			if (this === void 0 || this === null || typeof fun !== "function")
+				throw new TypeError();
+
+			var t = Object(this), len = t.length >>> 0;
+			for (var i = 0; i < len; i++) {
+				if (i in t)
+					fun.call(thisArg, t[i], i, t);
+			}
+		};
+	}
+
+	if (!Object.keys) {
+		Object.keys = (function () {
+			'use strict';
+
+			var hasOwnProperty = Object.prototype.hasOwnProperty,
+				hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'),
+				dontEnums = ['toString','toLocaleString','valueOf','hasOwnProperty',
+					'isPrototypeOf','propertyIsEnumerable','constructor'],
+				dontEnumsLength = dontEnums.length;
+
+			return function (obj) {
+				if (typeof obj !== 'object' && (typeof obj !== 'function' || obj === null)) {
+					throw new TypeError();
+				}
+				var result = [], prop, i;
+
+				for (prop in obj) {
+					if (hasOwnProperty.call(obj, prop)) {
+						result.push(prop);
+					}
+				}
+
+				if (hasDontEnumBug) {
+					for (i = 0; i < dontEnumsLength; i++) {
+						if (hasOwnProperty.call(obj, dontEnums[i])) {
+							result.push(dontEnums[i]);
+						}
+					}
+				}
+				return result;
+			};
+		}());
+	}
+
+	if (!String.prototype.trim) {
+		String.prototype.trim = function () {
+			return this.replace(/^\s+|\s+$/g, '');
+		};
+	}
+	if (!String.prototype.trimLeft) {
+		String.prototype.trimLeft = function() {
+			return this.replace(/^\s+/g, "");
+		};
+	}
+	if (!String.prototype.trimRight) {
+		String.prototype.trimRight = function() {
+			return this.replace(/\s+$/g, "");
+		};
+	}
+
+})(typeof self !== "undefined" && self || typeof window !== "undefined" && window || this);
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mda_heatmap_viz/static/javascript/lib/z-worker.js	Thu Apr 07 14:43:37 2016 -0400
@@ -0,0 +1,153 @@
+/* jshint worker:true */
+(function main(global) {
+	"use strict";
+
+	if (global.zWorkerInitialized)
+		throw new Error('z-worker.js should be run only once');
+	global.zWorkerInitialized = true;
+
+	addEventListener("message", function(event) {
+		var message = event.data, type = message.type, sn = message.sn;
+		var handler = handlers[type];
+		if (handler) {
+			try {
+				handler(message);
+			} catch (e) {
+				onError(type, sn, e);
+			}
+		}
+		//for debug
+		//postMessage({type: 'echo', originalType: type, sn: sn});
+	});
+
+	var handlers = {
+		importScripts: doImportScripts,
+		newTask: newTask,
+		append: processData,
+		flush: processData,
+	};
+
+	// deflater/inflater tasks indexed by serial numbers
+	var tasks = {};
+
+	function doImportScripts(msg) {
+		if (msg.scripts && msg.scripts.length > 0)
+			importScripts.apply(undefined, msg.scripts);
+		postMessage({type: 'importScripts'});
+	}
+
+	function newTask(msg) {
+		var CodecClass = global[msg.codecClass];
+		var sn = msg.sn;
+		if (tasks[sn])
+			throw Error('duplicated sn');
+		tasks[sn] =  {
+			codec: new CodecClass(msg.options),
+			crcInput: msg.crcType === 'input',
+			crcOutput: msg.crcType === 'output',
+			crc: new Crc32(),
+		};
+		postMessage({type: 'newTask', sn: sn});
+	}
+
+	// performance may not be supported
+	var now = global.performance ? global.performance.now.bind(global.performance) : Date.now;
+
+	function processData(msg) {
+		var sn = msg.sn, type = msg.type, input = msg.data;
+		var task = tasks[sn];
+		// allow creating codec on first append
+		if (!task && msg.codecClass) {
+			newTask(msg);
+			task = tasks[sn];
+		}
+		var isAppend = type === 'append';
+		var start = now();
+		var output;
+		if (isAppend) {
+			try {
+				output = task.codec.append(input, function onprogress(loaded) {
+					postMessage({type: 'progress', sn: sn, loaded: loaded});
+				});
+			} catch (e) {
+				delete tasks[sn];
+				throw e;
+			}
+		} else {
+			delete tasks[sn];
+			output = task.codec.flush();
+		}
+		var codecTime = now() - start;
+
+		start = now();
+		if (input && task.crcInput)
+			task.crc.append(input);
+		if (output && task.crcOutput)
+			task.crc.append(output);
+		var crcTime = now() - start;
+
+		var rmsg = {type: type, sn: sn, codecTime: codecTime, crcTime: crcTime};
+		var transferables = [];
+		if (output) {
+			rmsg.data = output;
+			transferables.push(output.buffer);
+		}
+		if (!isAppend && (task.crcInput || task.crcOutput))
+			rmsg.crc = task.crc.get();
+		
+		// posting a message with transferables will fail on IE10
+		try {
+			postMessage(rmsg, transferables);
+		} catch(ex) {
+			postMessage(rmsg); // retry without transferables
+		}
+	}
+
+	function onError(type, sn, e) {
+		var msg = {
+			type: type,
+			sn: sn,
+			error: formatError(e)
+		};
+		postMessage(msg);
+	}
+
+	function formatError(e) {
+		return { message: e.message, stack: e.stack };
+	}
+
+	// Crc32 code copied from file zip.js
+	function Crc32() {
+		this.crc = -1;
+	}
+	Crc32.prototype.append = function append(data) {
+		var crc = this.crc | 0, table = this.table;
+		for (var offset = 0, len = data.length | 0; offset < len; offset++)
+			crc = (crc >>> 8) ^ table[(crc ^ data[offset]) & 0xFF];
+		this.crc = crc;
+	};
+	Crc32.prototype.get = function get() {
+		return ~this.crc;
+	};
+	Crc32.prototype.table = (function() {
+		var i, j, t, table = []; // Uint32Array is actually slower than []
+		for (i = 0; i < 256; i++) {
+			t = i;
+			for (j = 0; j < 8; j++)
+				if (t & 1)
+					t = (t >>> 1) ^ 0xEDB88320;
+				else
+					t = t >>> 1;
+			table[i] = t;
+		}
+		return table;
+	})();
+
+	// "no-op" codec
+	function NOOP() {}
+	global.NOOP = NOOP;
+	NOOP.prototype.append = function append(bytes, onprogress) {
+		return bytes;
+	};
+	NOOP.prototype.flush = function flush() {};
+})(this);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mda_heatmap_viz/static/javascript/lib/zip.js	Thu Apr 07 14:43:37 2016 -0400
@@ -0,0 +1,966 @@
+/*
+ Copyright (c) 2013 Gildas Lormeau. All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+
+ 1. Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the distribution.
+
+ 3. The names of the authors may not be used to endorse or promote products
+ derived from this software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
+ INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+ FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT,
+ INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT,
+ INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
+ OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+ EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+(function(obj) {
+	"use strict";
+
+	var ERR_BAD_FORMAT = "File format is not recognized.";
+	var ERR_CRC = "CRC failed.";
+	var ERR_ENCRYPTED = "File contains encrypted entry.";
+	var ERR_ZIP64 = "File is using Zip64 (4gb+ file size).";
+	var ERR_READ = "Error while reading zip file.";
+	var ERR_WRITE = "Error while writing zip file.";
+	var ERR_WRITE_DATA = "Error while writing file data.";
+	var ERR_READ_DATA = "Error while reading file data.";
+	var ERR_DUPLICATED_NAME = "File already exists.";
+	var CHUNK_SIZE = 512 * 1024;
+	
+	var TEXT_PLAIN = "text/plain";
+
+	var appendABViewSupported;
+	try {
+		appendABViewSupported = new Blob([ new DataView(new ArrayBuffer(0)) ]).size === 0;
+	} catch (e) {
+	}
+
+	function Crc32() {
+		this.crc = -1;
+	}
+	Crc32.prototype.append = function append(data) {
+		var crc = this.crc | 0, table = this.table;
+		for (var offset = 0, len = data.length | 0; offset < len; offset++)
+			crc = (crc >>> 8) ^ table[(crc ^ data[offset]) & 0xFF];
+		this.crc = crc;
+	};
+	Crc32.prototype.get = function get() {
+		return ~this.crc;
+	};
+	Crc32.prototype.table = (function() {
+		var i, j, t, table = []; // Uint32Array is actually slower than []
+		for (i = 0; i < 256; i++) {
+			t = i;
+			for (j = 0; j < 8; j++)
+				if (t & 1)
+					t = (t >>> 1) ^ 0xEDB88320;
+				else
+					t = t >>> 1;
+			table[i] = t;
+		}
+		return table;
+	})();
+	
+	// "no-op" codec
+	function NOOP() {}
+	NOOP.prototype.append = function append(bytes, onprogress) {
+		return bytes;
+	};
+	NOOP.prototype.flush = function flush() {};
+
+	function blobSlice(blob, index, length) {
+		if (index < 0 || length < 0 || index + length > blob.size)
+			throw new RangeError('offset:' + index + ', length:' + length + ', size:' + blob.size);
+		if (blob.slice)
+			return blob.slice(index, index + length);
+		else if (blob.webkitSlice)
+			return blob.webkitSlice(index, index + length);
+		else if (blob.mozSlice)
+			return blob.mozSlice(index, index + length);
+		else if (blob.msSlice)
+			return blob.msSlice(index, index + length);
+	}
+
+	function getDataHelper(byteLength, bytes) {
+		var dataBuffer, dataArray;
+		dataBuffer = new ArrayBuffer(byteLength);
+		dataArray = new Uint8Array(dataBuffer);
+		if (bytes)
+			dataArray.set(bytes, 0);
+		return {
+			buffer : dataBuffer,
+			array : dataArray,
+			view : new DataView(dataBuffer)
+		};
+	}
+
+	// Readers
+	function Reader() {
+	}
+
+	function TextReader(text) {
+		var that = this, blobReader;
+
+		function init(callback, onerror) {
+			var blob = new Blob([ text ], {
+				type : TEXT_PLAIN
+			});
+			blobReader = new BlobReader(blob);
+			blobReader.init(function() {
+				that.size = blobReader.size;
+				callback();
+			}, onerror);
+		}
+
+		function readUint8Array(index, length, callback, onerror) {
+			blobReader.readUint8Array(index, length, callback, onerror);
+		}
+
+		that.size = 0;
+		that.init = init;
+		that.readUint8Array = readUint8Array;
+	}
+	TextReader.prototype = new Reader();
+	TextReader.prototype.constructor = TextReader;
+
+	function Data64URIReader(dataURI) {
+		var that = this, dataStart;
+
+		function init(callback) {
+			var dataEnd = dataURI.length;
+			while (dataURI.charAt(dataEnd - 1) == "=")
+				dataEnd--;
+			dataStart = dataURI.indexOf(",") + 1;
+			that.size = Math.floor((dataEnd - dataStart) * 0.75);
+			callback();
+		}
+
+		function readUint8Array(index, length, callback) {
+			var i, data = getDataHelper(length);
+			var start = Math.floor(index / 3) * 4;
+			var end = Math.ceil((index + length) / 3) * 4;
+			var bytes = obj.atob(dataURI.substring(start + dataStart, end + dataStart));
+			var delta = index - Math.floor(start / 4) * 3;
+			for (i = delta; i < delta + length; i++)
+				data.array[i - delta] = bytes.charCodeAt(i);
+			callback(data.array);
+		}
+
+		that.size = 0;
+		that.init = init;
+		that.readUint8Array = readUint8Array;
+	}
+	Data64URIReader.prototype = new Reader();
+	Data64URIReader.prototype.constructor = Data64URIReader;
+
+	function BlobReader(blob) {
+		var that = this;
+
+		function init(callback) {
+			that.size = blob.size;
+			callback();
+		}
+
+		function readUint8Array(index, length, callback, onerror) {
+			var reader = new FileReader();
+			reader.onload = function(e) {
+				callback(new Uint8Array(e.target.result));
+			};
+			reader.onerror = onerror;
+			try {
+				reader.readAsArrayBuffer(blobSlice(blob, index, length));
+			} catch (e) {
+				onerror(e);
+			}
+		}
+
+		that.size = 0;
+		that.init = init;
+		that.readUint8Array = readUint8Array;
+	}
+	BlobReader.prototype = new Reader();
+	BlobReader.prototype.constructor = BlobReader;
+
+	// Writers
+
+	function Writer() {
+	}
+	Writer.prototype.getData = function(callback) {
+		callback(this.data);
+	};
+
+	function TextWriter(encoding) {
+		var that = this, blob;
+
+		function init(callback) {
+			blob = new Blob([], {
+				type : TEXT_PLAIN
+			});
+			callback();
+		}
+
+		function writeUint8Array(array, callback) {
+			blob = new Blob([ blob, appendABViewSupported ? array : array.buffer ], {
+				type : TEXT_PLAIN
+			});
+			callback();
+		}
+
+		function getData(callback, onerror) {
+			var reader = new FileReader();
+			reader.onload = function(e) {
+				callback(e.target.result);
+			};
+			reader.onerror = onerror;
+			reader.readAsText(blob, encoding);
+		}
+
+		that.init = init;
+		that.writeUint8Array = writeUint8Array;
+		that.getData = getData;
+	}
+	TextWriter.prototype = new Writer();
+	TextWriter.prototype.constructor = TextWriter;
+
+	function Data64URIWriter(contentType) {
+		var that = this, data = "", pending = "";
+
+		function init(callback) {
+			data += "data:" + (contentType || "") + ";base64,";
+			callback();
+		}
+
+		function writeUint8Array(array, callback) {
+			var i, delta = pending.length, dataString = pending;
+			pending = "";
+			for (i = 0; i < (Math.floor((delta + array.length) / 3) * 3) - delta; i++)
+				dataString += String.fromCharCode(array[i]);
+			for (; i < array.length; i++)
+				pending += String.fromCharCode(array[i]);
+			if (dataString.length > 2)
+				data += obj.btoa(dataString);
+			else
+				pending = dataString;
+			callback();
+		}
+
+		function getData(callback) {
+			callback(data + obj.btoa(pending));
+		}
+
+		that.init = init;
+		that.writeUint8Array = writeUint8Array;
+		that.getData = getData;
+	}
+	Data64URIWriter.prototype = new Writer();
+	Data64URIWriter.prototype.constructor = Data64URIWriter;
+
+	function BlobWriter(contentType) {
+		var blob, that = this;
+
+		function init(callback) {
+			blob = new Blob([], {
+				type : contentType
+			});
+			callback();
+		}
+
+		function writeUint8Array(array, callback) {
+			blob = new Blob([ blob, appendABViewSupported ? array : array.buffer ], {
+				type : contentType
+			});
+			callback();
+		}
+
+		function getData(callback) {
+			callback(blob);
+		}
+
+		that.init = init;
+		that.writeUint8Array = writeUint8Array;
+		that.getData = getData;
+	}
+	BlobWriter.prototype = new Writer();
+	BlobWriter.prototype.constructor = BlobWriter;
+
+	/** 
+	 * inflate/deflate core functions
+	 * @param worker {Worker} web worker for the task.
+	 * @param initialMessage {Object} initial message to be sent to the worker. should contain
+	 *   sn(serial number for distinguishing multiple tasks sent to the worker), and codecClass.
+	 *   This function may add more properties before sending.
+	 */
+	function launchWorkerProcess(worker, initialMessage, reader, writer, offset, size, onprogress, onend, onreaderror, onwriteerror) {
+		var chunkIndex = 0, index, outputSize, sn = initialMessage.sn, crc;
+
+		function onflush() {
+			worker.removeEventListener('message', onmessage, false);
+			onend(outputSize, crc);
+		}
+
+		function onmessage(event) {
+			var message = event.data, data = message.data, err = message.error;
+			if (err) {
+				err.toString = function () { return 'Error: ' + this.message; };
+				onreaderror(err);
+				return;
+			}
+			if (message.sn !== sn)
+				return;
+			if (typeof message.codecTime === 'number')
+				worker.codecTime += message.codecTime; // should be before onflush()
+			if (typeof message.crcTime === 'number')
+				worker.crcTime += message.crcTime;
+
+			switch (message.type) {
+				case 'append':
+					if (data) {
+						outputSize += data.length;
+						writer.writeUint8Array(data, function() {
+							step();
+						}, onwriteerror);
+					} else
+						step();
+					break;
+				case 'flush':
+					crc = message.crc;
+					if (data) {
+						outputSize += data.length;
+						writer.writeUint8Array(data, function() {
+							onflush();
+						}, onwriteerror);
+					} else
+						onflush();
+					break;
+				case 'progress':
+					if (onprogress)
+						onprogress(index + message.loaded, size);
+					break;
+				case 'importScripts': //no need to handle here
+				case 'newTask':
+				case 'echo':
+					break;
+				default:
+					console.warn('zip.js:launchWorkerProcess: unknown message: ', message);
+			}
+		}
+
+		function step() {
+			index = chunkIndex * CHUNK_SIZE;
+			// use `<=` instead of `<`, because `size` may be 0.
+			if (index <= size) {
+				reader.readUint8Array(offset + index, Math.min(CHUNK_SIZE, size - index), function(array) {
+					if (onprogress)
+						onprogress(index, size);
+					var msg = index === 0 ? initialMessage : {sn : sn};
+					msg.type = 'append';
+					msg.data = array;
+					
+					// posting a message with transferables will fail on IE10
+					try {
+						worker.postMessage(msg, [array.buffer]);
+					} catch(ex) {
+						worker.postMessage(msg); // retry without transferables
+					}
+					chunkIndex++;
+				}, onreaderror);
+			} else {
+				worker.postMessage({
+					sn: sn,
+					type: 'flush'
+				});
+			}
+		}
+
+		outputSize = 0;
+		worker.addEventListener('message', onmessage, false);
+		step();
+	}
+
+	function launchProcess(process, reader, writer, offset, size, crcType, onprogress, onend, onreaderror, onwriteerror) {
+		var chunkIndex = 0, index, outputSize = 0,
+			crcInput = crcType === 'input',
+			crcOutput = crcType === 'output',
+			crc = new Crc32();
+		function step() {
+			var outputData;
+			index = chunkIndex * CHUNK_SIZE;
+			if (index < size)
+				reader.readUint8Array(offset + index, Math.min(CHUNK_SIZE, size - index), function(inputData) {
+					var outputData;
+					try {
+						outputData = process.append(inputData, function(loaded) {
+							if (onprogress)
+								onprogress(index + loaded, size);
+						});
+					} catch (e) {
+						onreaderror(e);
+						return;
+					}
+					if (outputData) {
+						outputSize += outputData.length;
+						writer.writeUint8Array(outputData, function() {
+							chunkIndex++;
+							setTimeout(step, 1);
+						}, onwriteerror);
+						if (crcOutput)
+							crc.append(outputData);
+					} else {
+						chunkIndex++;
+						setTimeout(step, 1);
+					}
+					if (crcInput)
+						crc.append(inputData);
+					if (onprogress)
+						onprogress(index, size);
+				}, onreaderror);
+			else {
+				try {
+					outputData = process.flush();
+				} catch (e) {
+					onreaderror(e);
+					return;
+				}
+				if (outputData) {
+					if (crcOutput)
+						crc.append(outputData);
+					outputSize += outputData.length;
+					writer.writeUint8Array(outputData, function() {
+						onend(outputSize, crc.get());
+					}, onwriteerror);
+				} else
+					onend(outputSize, crc.get());
+			}
+		}
+
+		step();
+	}
+
+	function inflate(worker, sn, reader, writer, offset, size, computeCrc32, onend, onprogress, onreaderror, onwriteerror) {
+		var crcType = computeCrc32 ? 'output' : 'none';
+		if (obj.zip.useWebWorkers) {
+			var initialMessage = {
+				sn: sn,
+				codecClass: 'Inflater',
+				crcType: crcType,
+			};
+			launchWorkerProcess(worker, initialMessage, reader, writer, offset, size, onprogress, onend, onreaderror, onwriteerror);
+		} else
+			launchProcess(new obj.zip.Inflater(), reader, writer, offset, size, crcType, onprogress, onend, onreaderror, onwriteerror);
+	}
+
+	function deflate(worker, sn, reader, writer, level, onend, onprogress, onreaderror, onwriteerror) {
+		var crcType = 'input';
+		if (obj.zip.useWebWorkers) {
+			var initialMessage = {
+				sn: sn,
+				options: {level: level},
+				codecClass: 'Deflater',
+				crcType: crcType,
+			};
+			launchWorkerProcess(worker, initialMessage, reader, writer, 0, reader.size, onprogress, onend, onreaderror, onwriteerror);
+		} else
+			launchProcess(new obj.zip.Deflater(), reader, writer, 0, reader.size, crcType, onprogress, onend, onreaderror, onwriteerror);
+	}
+
+	function copy(worker, sn, reader, writer, offset, size, computeCrc32, onend, onprogress, onreaderror, onwriteerror) {
+		var crcType = 'input';
+		if (obj.zip.useWebWorkers && computeCrc32) {
+			var initialMessage = {
+				sn: sn,
+				codecClass: 'NOOP',
+				crcType: crcType,
+			};
+			launchWorkerProcess(worker, initialMessage, reader, writer, offset, size, onprogress, onend, onreaderror, onwriteerror);
+		} else
+			launchProcess(new NOOP(), reader, writer, offset, size, crcType, onprogress, onend, onreaderror, onwriteerror);
+	}
+
+	// ZipReader
+
+	function decodeASCII(str) {
+		var i, out = "", charCode, extendedASCII = [ '\u00C7', '\u00FC', '\u00E9', '\u00E2', '\u00E4', '\u00E0', '\u00E5', '\u00E7', '\u00EA', '\u00EB',
+				'\u00E8', '\u00EF', '\u00EE', '\u00EC', '\u00C4', '\u00C5', '\u00C9', '\u00E6', '\u00C6', '\u00F4', '\u00F6', '\u00F2', '\u00FB', '\u00F9',
+				'\u00FF', '\u00D6', '\u00DC', '\u00F8', '\u00A3', '\u00D8', '\u00D7', '\u0192', '\u00E1', '\u00ED', '\u00F3', '\u00FA', '\u00F1', '\u00D1',
+				'\u00AA', '\u00BA', '\u00BF', '\u00AE', '\u00AC', '\u00BD', '\u00BC', '\u00A1', '\u00AB', '\u00BB', '_', '_', '_', '\u00A6', '\u00A6',
+				'\u00C1', '\u00C2', '\u00C0', '\u00A9', '\u00A6', '\u00A6', '+', '+', '\u00A2', '\u00A5', '+', '+', '-', '-', '+', '-', '+', '\u00E3',
+				'\u00C3', '+', '+', '-', '-', '\u00A6', '-', '+', '\u00A4', '\u00F0', '\u00D0', '\u00CA', '\u00CB', '\u00C8', 'i', '\u00CD', '\u00CE',
+				'\u00CF', '+', '+', '_', '_', '\u00A6', '\u00CC', '_', '\u00D3', '\u00DF', '\u00D4', '\u00D2', '\u00F5', '\u00D5', '\u00B5', '\u00FE',
+				'\u00DE', '\u00DA', '\u00DB', '\u00D9', '\u00FD', '\u00DD', '\u00AF', '\u00B4', '\u00AD', '\u00B1', '_', '\u00BE', '\u00B6', '\u00A7',
+				'\u00F7', '\u00B8', '\u00B0', '\u00A8', '\u00B7', '\u00B9', '\u00B3', '\u00B2', '_', ' ' ];
+		for (i = 0; i < str.length; i++) {
+			charCode = str.charCodeAt(i) & 0xFF;
+			if (charCode > 127)
+				out += extendedASCII[charCode - 128];
+			else
+				out += String.fromCharCode(charCode);
+		}
+		return out;
+	}
+
+	function decodeUTF8(string) {
+		return decodeURIComponent(escape(string));
+	}
+
+	function getString(bytes) {
+		var i, str = "";
+		for (i = 0; i < bytes.length; i++)
+			str += String.fromCharCode(bytes[i]);
+		return str;
+	}
+
+	function getDate(timeRaw) {
+		var date = (timeRaw & 0xffff0000) >> 16, time = timeRaw & 0x0000ffff;
+		try {
+			return new Date(1980 + ((date & 0xFE00) >> 9), ((date & 0x01E0) >> 5) - 1, date & 0x001F, (time & 0xF800) >> 11, (time & 0x07E0) >> 5,
+					(time & 0x001F) * 2, 0);
+		} catch (e) {
+		}
+	}
+
+	function readCommonHeader(entry, data, index, centralDirectory, onerror) {
+		entry.version = data.view.getUint16(index, true);
+		entry.bitFlag = data.view.getUint16(index + 2, true);
+		entry.compressionMethod = data.view.getUint16(index + 4, true);
+		entry.lastModDateRaw = data.view.getUint32(index + 6, true);
+		entry.lastModDate = getDate(entry.lastModDateRaw);
+		if ((entry.bitFlag & 0x01) === 0x01) {
+			onerror(ERR_ENCRYPTED);
+			return;
+		}
+		if (centralDirectory || (entry.bitFlag & 0x0008) != 0x0008) {
+			entry.crc32 = data.view.getUint32(index + 10, true);
+			entry.compressedSize = data.view.getUint32(index + 14, true);
+			entry.uncompressedSize = data.view.getUint32(index + 18, true);
+		}
+		if (entry.compressedSize === 0xFFFFFFFF || entry.uncompressedSize === 0xFFFFFFFF) {
+			onerror(ERR_ZIP64);
+			return;
+		}
+		entry.filenameLength = data.view.getUint16(index + 22, true);
+		entry.extraFieldLength = data.view.getUint16(index + 24, true);
+	}
+
+	function createZipReader(reader, callback, onerror) {
+		var inflateSN = 0;
+
+		function Entry() {
+		}
+
+		Entry.prototype.getData = function(writer, onend, onprogress, checkCrc32) {
+			var that = this;
+
+			function testCrc32(crc32) {
+				var dataCrc32 = getDataHelper(4);
+				dataCrc32.view.setUint32(0, crc32);
+				return that.crc32 == dataCrc32.view.getUint32(0);
+			}
+
+			function getWriterData(uncompressedSize, crc32) {
+				if (checkCrc32 && !testCrc32(crc32))
+					onerror(ERR_CRC);
+				else
+					writer.getData(function(data) {
+						onend(data);
+					});
+			}
+
+			function onreaderror(err) {
+				onerror(err || ERR_READ_DATA);
+			}
+
+			function onwriteerror(err) {
+				onerror(err || ERR_WRITE_DATA);
+			}
+
+			reader.readUint8Array(that.offset, 30, function(bytes) {
+				var data = getDataHelper(bytes.length, bytes), dataOffset;
+				if (data.view.getUint32(0) != 0x504b0304) {
+					onerror(ERR_BAD_FORMAT);
+					return;
+				}
+				readCommonHeader(that, data, 4, false, onerror);
+				dataOffset = that.offset + 30 + that.filenameLength + that.extraFieldLength;
+				writer.init(function() {
+					if (that.compressionMethod === 0)
+						copy(that._worker, inflateSN++, reader, writer, dataOffset, that.compressedSize, checkCrc32, getWriterData, onprogress, onreaderror, onwriteerror);
+					else
+						inflate(that._worker, inflateSN++, reader, writer, dataOffset, that.compressedSize, checkCrc32, getWriterData, onprogress, onreaderror, onwriteerror);
+				}, onwriteerror);
+			}, onreaderror);
+		};
+
+		function seekEOCDR(eocdrCallback) {
+			// "End of central directory record" is the last part of a zip archive, and is at least 22 bytes long.
+			// Zip file comment is the last part of EOCDR and has max length of 64KB,
+			// so we only have to search the last 64K + 22 bytes of a archive for EOCDR signature (0x06054b50).
+			var EOCDR_MIN = 22;
+			if (reader.size < EOCDR_MIN) {
+				onerror(ERR_BAD_FORMAT);
+				return;
+			}
+			var ZIP_COMMENT_MAX = 256 * 256, EOCDR_MAX = EOCDR_MIN + ZIP_COMMENT_MAX;
+
+			// In most cases, the EOCDR is EOCDR_MIN bytes long
+			doSeek(EOCDR_MIN, function() {
+				// If not found, try within EOCDR_MAX bytes
+				doSeek(Math.min(EOCDR_MAX, reader.size), function() {
+					onerror(ERR_BAD_FORMAT);
+				});
+			});
+
+			// seek last length bytes of file for EOCDR
+			function doSeek(length, eocdrNotFoundCallback) {
+				reader.readUint8Array(reader.size - length, length, function(bytes) {
+					for (var i = bytes.length - EOCDR_MIN; i >= 0; i--) {
+						if (bytes[i] === 0x50 && bytes[i + 1] === 0x4b && bytes[i + 2] === 0x05 && bytes[i + 3] === 0x06) {
+							eocdrCallback(new DataView(bytes.buffer, i, EOCDR_MIN));
+							return;
+						}
+					}
+					eocdrNotFoundCallback();
+				}, function() {
+					onerror(ERR_READ);
+				});
+			}
+		}
+
+		var zipReader = {
+			getEntries : function(callback) {
+				var worker = this._worker;
+				// look for End of central directory record
+				seekEOCDR(function(dataView) {
+					var datalength, fileslength;
+					datalength = dataView.getUint32(16, true);
+					fileslength = dataView.getUint16(8, true);
+					if (datalength < 0 || datalength >= reader.size) {
+						onerror(ERR_BAD_FORMAT);
+						return;
+					}
+					reader.readUint8Array(datalength, reader.size - datalength, function(bytes) {
+						var i, index = 0, entries = [], entry, filename, comment, data = getDataHelper(bytes.length, bytes);
+						for (i = 0; i < fileslength; i++) {
+							entry = new Entry();
+							entry._worker = worker;
+							if (data.view.getUint32(index) != 0x504b0102) {
+								onerror(ERR_BAD_FORMAT);
+								return;
+							}
+							readCommonHeader(entry, data, index + 6, true, onerror);
+							entry.commentLength = data.view.getUint16(index + 32, true);
+							entry.directory = ((data.view.getUint8(index + 38) & 0x10) == 0x10);
+							entry.offset = data.view.getUint32(index + 42, true);
+							filename = getString(data.array.subarray(index + 46, index + 46 + entry.filenameLength));
+							entry.filename = ((entry.bitFlag & 0x0800) === 0x0800) ? decodeUTF8(filename) : decodeASCII(filename);
+							if (!entry.directory && entry.filename.charAt(entry.filename.length - 1) == "/")
+								entry.directory = true;
+							comment = getString(data.array.subarray(index + 46 + entry.filenameLength + entry.extraFieldLength, index + 46
+									+ entry.filenameLength + entry.extraFieldLength + entry.commentLength));
+							entry.comment = ((entry.bitFlag & 0x0800) === 0x0800) ? decodeUTF8(comment) : decodeASCII(comment);
+							entries.push(entry);
+							index += 46 + entry.filenameLength + entry.extraFieldLength + entry.commentLength;
+						}
+						callback(entries);
+					}, function() {
+						onerror(ERR_READ);
+					});
+				});
+			},
+			close : function(callback) {
+				if (this._worker) {
+					this._worker.terminate();
+					this._worker = null;
+				}
+				if (callback)
+					callback();
+			},
+			_worker: null
+		};
+
+		if (!obj.zip.useWebWorkers)
+			callback(zipReader);
+		else {
+			createWorker('inflater',
+				function(worker) {
+					zipReader._worker = worker;
+					callback(zipReader);
+				},
+				function(err) {
+					onerror(err);
+				}
+			);
+		}
+	}
+
+	// ZipWriter
+
+	function encodeUTF8(string) {
+		return unescape(encodeURIComponent(string));
+	}
+
+	function getBytes(str) {
+		var i, array = [];
+		for (i = 0; i < str.length; i++)
+			array.push(str.charCodeAt(i));
+		return array;
+	}
+
+	function createZipWriter(writer, callback, onerror, dontDeflate) {
+		var files = {}, filenames = [], datalength = 0;
+		var deflateSN = 0;
+
+		function onwriteerror(err) {
+			onerror(err || ERR_WRITE);
+		}
+
+		function onreaderror(err) {
+			onerror(err || ERR_READ_DATA);
+		}
+
+		var zipWriter = {
+			add : function(name, reader, onend, onprogress, options) {
+				var header, filename, date;
+				var worker = this._worker;
+
+				function writeHeader(callback) {
+					var data;
+					date = options.lastModDate || new Date();
+					header = getDataHelper(26);
+					files[name] = {
+						headerArray : header.array,
+						directory : options.directory,
+						filename : filename,
+						offset : datalength,
+						comment : getBytes(encodeUTF8(options.comment || ""))
+					};
+					header.view.setUint32(0, 0x14000808);
+					if (options.version)
+						header.view.setUint8(0, options.version);
+					if (!dontDeflate && options.level !== 0 && !options.directory)
+						header.view.setUint16(4, 0x0800);
+					header.view.setUint16(6, (((date.getHours() << 6) | date.getMinutes()) << 5) | date.getSeconds() / 2, true);
+					header.view.setUint16(8, ((((date.getFullYear() - 1980) << 4) | (date.getMonth() + 1)) << 5) | date.getDate(), true);
+					header.view.setUint16(22, filename.length, true);
+					data = getDataHelper(30 + filename.length);
+					data.view.setUint32(0, 0x504b0304);
+					data.array.set(header.array, 4);
+					data.array.set(filename, 30);
+					datalength += data.array.length;
+					writer.writeUint8Array(data.array, callback, onwriteerror);
+				}
+
+				function writeFooter(compressedLength, crc32) {
+					var footer = getDataHelper(16);
+					datalength += compressedLength || 0;
+					footer.view.setUint32(0, 0x504b0708);
+					if (typeof crc32 != "undefined") {
+						header.view.setUint32(10, crc32, true);
+						footer.view.setUint32(4, crc32, true);
+					}
+					if (reader) {
+						footer.view.setUint32(8, compressedLength, true);
+						header.view.setUint32(14, compressedLength, true);
+						footer.view.setUint32(12, reader.size, true);
+						header.view.setUint32(18, reader.size, true);
+					}
+					writer.writeUint8Array(footer.array, function() {
+						datalength += 16;
+						onend();
+					}, onwriteerror);
+				}
+
+				function writeFile() {
+					options = options || {};
+					name = name.trim();
+					if (options.directory && name.charAt(name.length - 1) != "/")
+						name += "/";
+					if (files.hasOwnProperty(name)) {
+						onerror(ERR_DUPLICATED_NAME);
+						return;
+					}
+					filename = getBytes(encodeUTF8(name));
+					filenames.push(name);
+					writeHeader(function() {
+						if (reader)
+							if (dontDeflate || options.level === 0)
+								copy(worker, deflateSN++, reader, writer, 0, reader.size, true, writeFooter, onprogress, onreaderror, onwriteerror);
+							else
+								deflate(worker, deflateSN++, reader, writer, options.level, writeFooter, onprogress, onreaderror, onwriteerror);
+						else
+							writeFooter();
+					}, onwriteerror);
+				}
+
+				if (reader)
+					reader.init(writeFile, onreaderror);
+				else
+					writeFile();
+			},
+			close : function(callback) {
+				if (this._worker) {
+					this._worker.terminate();
+					this._worker = null;
+				}
+
+				var data, length = 0, index = 0, indexFilename, file;
+				for (indexFilename = 0; indexFilename < filenames.length; indexFilename++) {
+					file = files[filenames[indexFilename]];
+					length += 46 + file.filename.length + file.comment.length;
+				}
+				data = getDataHelper(length + 22);
+				for (indexFilename = 0; indexFilename < filenames.length; indexFilename++) {
+					file = files[filenames[indexFilename]];
+					data.view.setUint32(index, 0x504b0102);
+					data.view.setUint16(index + 4, 0x1400);
+					data.array.set(file.headerArray, index + 6);
+					data.view.setUint16(index + 32, file.comment.length, true);
+					if (file.directory)
+						data.view.setUint8(index + 38, 0x10);
+					data.view.setUint32(index + 42, file.offset, true);
+					data.array.set(file.filename, index + 46);
+					data.array.set(file.comment, index + 46 + file.filename.length);
+					index += 46 + file.filename.length + file.comment.length;
+				}
+				data.view.setUint32(index, 0x504b0506);
+				data.view.setUint16(index + 8, filenames.length, true);
+				data.view.setUint16(index + 10, filenames.length, true);
+				data.view.setUint32(index + 12, length, true);
+				data.view.setUint32(index + 16, datalength, true);
+				writer.writeUint8Array(data.array, function() {
+					writer.getData(callback);
+				}, onwriteerror);
+			},
+			_worker: null
+		};
+
+		if (!obj.zip.useWebWorkers)
+			callback(zipWriter);
+		else {
+			createWorker('deflater',
+				function(worker) {
+					zipWriter._worker = worker;
+					callback(zipWriter);
+				},
+				function(err) {
+					onerror(err);
+				}
+			);
+		}
+	}
+
+	function resolveURLs(urls) {
+		var a = document.createElement('a');
+		return urls.map(function(url) {
+			a.href = url;
+			return a.href;
+		});
+	}
+
+	var DEFAULT_WORKER_SCRIPTS = {
+		deflater: ['z-worker.js', 'deflate.js'],
+		inflater: ['z-worker.js', 'inflate.js']
+	};
+	function createWorker(type, callback, onerror) {
+		if (obj.zip.workerScripts !== null && obj.zip.workerScriptsPath !== null) {
+			onerror(new Error('Either zip.workerScripts or zip.workerScriptsPath may be set, not both.'));
+			return;
+		}
+		var scripts;
+		if (obj.zip.workerScripts) {
+			scripts = obj.zip.workerScripts[type];
+			if (!Array.isArray(scripts)) {
+				onerror(new Error('zip.workerScripts.' + type + ' is not an array!'));
+				return;
+			}
+			scripts = resolveURLs(scripts);
+		} else {
+			scripts = DEFAULT_WORKER_SCRIPTS[type].slice(0);
+			scripts[0] = (obj.zip.workerScriptsPath || '') + scripts[0];
+		}
+		var worker = new Worker(scripts[0]);
+		// record total consumed time by inflater/deflater/crc32 in this worker
+		worker.codecTime = worker.crcTime = 0;
+		worker.postMessage({ type: 'importScripts', scripts: scripts.slice(1) });
+		worker.addEventListener('message', onmessage);
+		function onmessage(ev) {
+			var msg = ev.data;
+			if (msg.error) {
+				worker.terminate(); // should before onerror(), because onerror() may throw.
+				onerror(msg.error);
+				return;
+			}
+			if (msg.type === 'importScripts') {
+				worker.removeEventListener('message', onmessage);
+				worker.removeEventListener('error', errorHandler);
+				callback(worker);
+			}
+		}
+		// catch entry script loading error and other unhandled errors
+		worker.addEventListener('error', errorHandler);
+		function errorHandler(err) {
+			worker.terminate();
+			onerror(err);
+		}
+	}
+
+	function onerror_default(error) {
+		console.error(error);
+	}
+	obj.zip = {
+		Reader : Reader,
+		Writer : Writer,
+		BlobReader : BlobReader,
+		Data64URIReader : Data64URIReader,
+		TextReader : TextReader,
+		BlobWriter : BlobWriter,
+		Data64URIWriter : Data64URIWriter,
+		TextWriter : TextWriter,
+		createReader : function(reader, callback, onerror) {
+			onerror = onerror || onerror_default;
+
+			reader.init(function() {
+				createZipReader(reader, callback, onerror);
+			}, onerror);
+		},
+		createWriter : function(writer, callback, onerror, dontDeflate) {
+			onerror = onerror || onerror_default;
+			dontDeflate = !!dontDeflate;
+
+			writer.init(function() {
+				createZipWriter(writer, callback, onerror, dontDeflate);
+			}, onerror);
+		},
+		useWebWorkers : true,
+		/**
+		 * Directory containing the default worker scripts (z-worker.js, deflate.js, and inflate.js), relative to current base url.
+		 * E.g.: zip.workerScripts = './';
+		 */
+		workerScriptsPath : null,
+		/**
+		 * Advanced option to control which scripts are loaded in the Web worker. If this option is specified, then workerScriptsPath must not be set.
+		 * workerScripts.deflater/workerScripts.inflater should be arrays of urls to scripts for deflater/inflater, respectively.
+		 * Scripts in the array are executed in order, and the first one should be z-worker.js, which is used to start the worker.
+		 * All urls are relative to current base url.
+		 * E.g.:
+		 * zip.workerScripts = {
+		 *   deflater: ['z-worker.js', 'deflate.js'],
+		 *   inflater: ['z-worker.js', 'inflate.js']
+		 * };
+		 */
+		workerScripts : null,
+	};
+
+})(this);
Binary file mda_heatmap_viz/templates/._mda_heatmap_viz.mako has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mda_heatmap_viz/templates/MDAheatmap.oldmako	Thu Apr 07 14:43:37 2016 -0400
@@ -0,0 +1,98 @@
+<HTML>
+   <HEAD>
+      <link rel="stylesheet" href="/plugins/visualizations/MDAheatmap/static/css/NGCHM.css">
+      <script src="/plugins/visualizations/MDAheatmap/static/javascript/lib/zip.js"></script>
+      <script src="/plugins/visualizations/MDAheatmap/static/javascript/lib/inflate.js"></script>
+      <script src="/plugins/visualizations/MDAheatmap/static/javascript/NGCHM_Util.js"></script>
+      <script src="/plugins/visualizations/MDAheatmap/static/javascript/SelectionManager.js"></script>
+      <script src="/plugins/visualizations/MDAheatmap/static/javascript/MatrixManager.js"></script>
+      <script src="/plugins/visualizations/MDAheatmap/static/javascript/ColorMapManager.js"></script>
+      <script src="/plugins/visualizations/MDAheatmap/static/javascript/SummaryHeatMapDisplay.js"></script>
+      <script src="/plugins/visualizations/MDAheatmap/static/javascript/DetailHeatMapDisplay.js"></script>
+      <script src="/plugins/visualizations/MDAheatmap/static/javascript/UserHelpManager.js"></script>
+      <script src="/plugins/visualizations/MDAheatmap/static/javascript/UserPreferenceManager.js"></script>
+
+	<meta id='viewport' name ="viewport" content="">
+
+   </HEAD>
+   
+   <BODY onresize="chmResize()">
+    <%
+       from galaxy import model
+       users_current_history = trans.history
+       url_dict = { }
+       dataset_ids = [ trans.security.encode_id( d.id ) for d in users_current_history.datasets ]
+       output_datasets = hda.creating_job.output_datasets
+       for o in output_datasets:
+              url_dict[ o.name ] = trans.security.encode_id( o.dataset_id )
+    %>
+
+    <script>
+       heatMap = null;  //global - heatmap object.
+
+       var url_dict = ${ h.dumps( url_dict ) };
+       var hdaId   = '${trans.security.encode_id( hda.id )}';
+       var hdaExt  = '${hda.ext}';
+       var ajaxUrl = "${h.url_for( controller='/datasets', action='index')}/" + hdaId + "/display?to_ext=" + hdaExt;
+
+       var xmlhttp=new XMLHttpRequest();
+       xmlhttp.open("GET", ajaxUrl, true);
+       xmlhttp.responseType = 'blob';
+       xmlhttp.onload = function(e) {
+           if (this.status == 200) {
+               var blob = new Blob([this.response], {type: 'compress/zip'});
+               zip.useWebWorkers = false;
+               var matrixMgr = new MatrixManager(MatrixManager.FILE_SOURCE);
+               var name = 'NGCHM';
+               heatMap = matrixMgr.getHeatMap(name,  processSummaryMapUpdate, blob);
+               heatMap.addEventListener(processDetailMapUpdate);
+               initSummaryDisplay();
+               initDetailDisplay()
+           }
+       };
+       xmlhttp.send();
+
+       function chmResize() {
+          detailResize();
+       }
+
+    </script>
+
+    <div class="mdaServiceHeader">
+        <div class="mdaServiceHeaderLogo">
+            <img src="/plugins/visualizations/MDAheatmap/static/images/mdandersonlogo260x85.png" alt="">
+        </div>
+      
+    	   <div id='detail_buttons' align="center" style="display:none">
+ 			<img id='zoomOut_btn' src='/plugins/visualizations/MDAheatmap/static/images/zoom-out.png' alt='Zoom Out' onmouseover='detailDataToolHelp(this,"Zoom Out")' onclick='detailDataZoomOut();'   align="top"   />
+		    <img id='zoomIn_btn' src='/plugins/visualizations/MDAheatmap/static/images/zoom-in.png' alt='Zoom In' onmouseover='detailDataToolHelp(this,"Zoom In")' onclick='detailDataZoomIn();' align="top"   />
+		    <img id='full_btn' src='/plugins/visualizations/MDAheatmap/static/images/full_selected.png' alt='Full' onmouseover='detailDataToolHelp(this,"Normal View")' onclick='detailNormal();' align="top"   />
+		    <img id='ribbonH_btn' src='/plugins/visualizations/MDAheatmap/static/images/ribbonH.png' alt='Ribbon H' onmouseover='detailDataToolHelp(this,"Horizontal Ribbon View")' onclick='detailHRibbonButton();' align="top"  />
+		    <img id='ribbonV_btn' src='/plugins/visualizations/MDAheatmap/static/images/ribbonV.png' alt='Ribbon V' onmouseover='detailDataToolHelp(this,"Vertical Ribbon View")' onclick='detailVRibbonButton();'  align="top"  />
+   			<span style='display: inline-block;'><b>Search: </b><input type="text" id="search_text" name="search" onkeypress='clearSrchBtns();' onchange='detailSearch();'
+   			                                                     onmouseover='detailDataToolHelp(this,"Search Row/Column Labels. Separate search terms with spaces or commas. Use * for wild card matching. Hit enter or Go to run the search. If the search box turns red none of the search terms were found. If it turns yellow only some of the search terms were found.", 200)' ></span>	
+		    <img id='go_btn' src='/plugins/visualizations/MDAheatmap/static/images/go.png' alt='Go' onmouseover='detailDataToolHelp(this,"Search Row/Column Labels")'  onclick='detailSearch();' align="top"  />
+		    <img id='prev_btn' src='/plugins/visualizations/MDAheatmap/static/images/prev.png' alt='Previous' onmouseover='userHelpClose();' style="display:none;" onclick='searchPrev();'  align="top"  />
+		    <img id='next_btn' src='/plugins/visualizations/MDAheatmap/static/images/next.png' alt='Next' onmouseover='userHelpClose();' style="display:none;" onclick='searchNext();'  align="top"  />
+		    <img id='cancel_btn' src='/plugins/visualizations/MDAheatmap/static/images/cancel.png' alt='Cancel' onmouseover='detailDataToolHelp(this,"Clear current search")' style="display:none;" onclick='clearSearch();'  align="top"  />
+       </div>
+    </div>
+
+    <div id="container">
+
+       <div id='summary_chm' style='position: relative;'>
+          <canvas id='summary_canvas'></canvas>
+		<div id='sumlabelDiv' style="display: inline-block"></div>
+       </div>
+
+	  <div id= 'divider' style='position: relative;' onmousedown="dividerStart()" ontouchstart="dividerStart()">
+	  </div>
+
+       <div id='detail_chm' style='position: relative;'>
+          <canvas id='detail_canvas' style='display: inline-block'></canvas>
+          <div id='labelDiv' style="display: inline-block"></div>
+       </div>
+   </div>
+
+</BODY >
+</HTML>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mda_heatmap_viz/templates/mda_heatmap_viz.mako	Thu Apr 07 14:43:37 2016 -0400
@@ -0,0 +1,145 @@
+<HTML>
+   <HEAD>
+      <link rel="stylesheet" href="/plugins/visualizations/mda_heatmap_viz/static/css/NGCHM.css">
+      <script src="/plugins/visualizations/mda_heatmap_viz/static/javascript/lib/zip.js"></script>
+      <script src="/plugins/visualizations/mda_heatmap_viz/static/javascript/lib/inflate.js"></script>
+      <script src="/plugins/visualizations/mda_heatmap_viz/static/javascript/NGCHM_Util.js"></script>
+      <script src="/plugins/visualizations/mda_heatmap_viz/static/javascript/SelectionManager.js"></script>
+      <script src="/plugins/visualizations/mda_heatmap_viz/static/javascript/MatrixManager.js"></script>
+      <script src="/plugins/visualizations/mda_heatmap_viz/static/javascript/ColorMapManager.js"></script>
+      <script src="/plugins/visualizations/mda_heatmap_viz/static/javascript/SummaryHeatMapDisplay.js"></script>
+      <script src="/plugins/visualizations/mda_heatmap_viz/static/javascript/DetailHeatMapDisplay.js"></script>
+      <script src="/plugins/visualizations/mda_heatmap_viz/static/javascript/UserHelpManager.js"></script>
+      <script src="/plugins/visualizations/mda_heatmap_viz/static/javascript/UserPreferenceManager.js"></script>
+      <script src="/plugins/visualizations/mda_heatmap_viz/static/javascript/PdfGenerator.js"></script>
+      <script src="/plugins/visualizations/mda_heatmap_viz/static/javascript/lib/jspdf.debug.js"></script>
+ 	 <script src="/plugins/visualizations/mda_heatmap_viz/static/javascript/custom.js"></script>
+
+	 <meta id='viewport' name ="viewport" content="">
+
+   </HEAD>
+   
+   <BODY onresize="chmResize()">
+    <%
+       from galaxy import model
+       users_current_history = trans.history
+       url_dict = { }
+       dataset_ids = [ trans.security.encode_id( d.id ) for d in users_current_history.datasets ]
+       output_datasets = hda.creating_job.output_datasets
+       for o in output_datasets:
+              url_dict[ o.name ] = trans.security.encode_id( o.dataset_id )
+    %>
+
+    <script>
+       heatMap = null;  //global - heatmap object.
+ 	  staticPath = "/plugins/visualizations/mda_heatmap_viz/static/"; //path for static web content - changes in galaxy.
+
+       var url_dict = ${ h.dumps( url_dict ) };
+       var hdaId   = '${trans.security.encode_id( hda.id )}';
+       var hdaExt  = '${hda.ext}';
+       var ajaxUrl = "${h.url_for( controller='/datasets', action='index')}/" + hdaId + "/display?to_ext=" + hdaExt;
+
+       var xmlhttp=new XMLHttpRequest();
+       xmlhttp.open("GET", ajaxUrl, true);
+       xmlhttp.responseType = 'blob';
+       xmlhttp.onload = function(e) {
+           if (this.status == 200) {
+               var blob = new Blob([this.response], {type: 'compress/zip'});
+               zip.useWebWorkers = false;
+               var matrixMgr = new MatrixManager(MatrixManager.FILE_SOURCE);
+               var name = this.getResponseHeader("Content-Disposition");
+               if (name.indexOf('[') > -1) {
+                 name = name.substring(name.indexOf('[')+1, name.indexOf(']'));
+               }               heatMap = matrixMgr.getHeatMap(name,  processSummaryMapUpdate, blob);
+               heatMap.addEventListener(processDetailMapUpdate);
+               initSummaryDisplay();
+               initDetailDisplay()
+           }
+       };
+       xmlhttp.send();
+
+       function chmResize() {
+          detailResize();
+       }
+
+    </script>
+
+    <div class="mdaServiceHeader">
+        <div class="mdaServiceHeaderLogo">
+            <img src="/plugins/visualizations/mda_heatmap_viz/static/images/mdandersonlogo260x85.png" alt="">
+        </div>
+      
+	   <div id='detail_buttons' align="center" style="display:none">
+ 		    <img id='zoomOut_btn' src='/plugins/visualizations/mda_heatmap_viz/static/images/zoom-out.png' alt='Zoom Out' onmouseover='detailDataToolHelp(this,"Zoom Out")' onclick='detailDataZoomOut();'   align="top"   />
+		    <img id='zoomIn_btn' src='/plugins/visualizations/mda_heatmap_viz/static/images/zoom-in.png' alt='Zoom In' onmouseover='detailDataToolHelp(this,"Zoom In")' onclick='detailDataZoomIn();' align="top"   />
+		    <img id='full_btn' src='/plugins/visualizations/mda_heatmap_viz/static/images/full_selected.png' alt='Full' onmouseover='detailDataToolHelp(this,"Normal View")' onclick='detailNormal();' align="top"   />
+		    <img id='ribbonH_btn' src='/plugins/visualizations/mda_heatmap_viz/static/images/ribbonH.png' alt='Ribbon H' onmouseover='detailDataToolHelp(this,"Horizontal Ribbon View")' onclick='detailHRibbonButton();' align="top"  />
+		    <img id='ribbonV_btn' src='/plugins/visualizations/mda_heatmap_viz/static/images/ribbonV.png' alt='Ribbon V' onmouseover='detailDataToolHelp(this,"Vertical Ribbon View")' onclick='detailVRibbonButton();'  align="top"  />
+   		    <span style='display: inline-block;'><b>Search: </b><input type="text" id="search_text" name="search" onkeypress='clearSrchBtns();' onchange='detailSearch();'
+   			                                                     onmouseover='detailDataToolHelp(this,"Search Row/Column Labels. Separate search terms with spaces or commas. Use * for wild card matching. Hit enter or Go to run the search. If the search box turns red none of the search terms were found. If it turns yellow only some of the search terms were found.", 200)' ></span>	
+		    <img id='go_btn' src='/plugins/visualizations/mda_heatmap_viz/static/images/go.png' alt='Go' onmouseover='detailDataToolHelp(this,"Search Row/Column Labels")'  onclick='detailSearch();' align="top"  />
+		    <img id='prev_btn' src='/plugins/visualizations/mda_heatmap_viz/static/images/prev.png' alt='Previous' onmouseover='userHelpClose();' style="display:none;" onclick='searchPrev();'  align="top"  />
+		    <img id='next_btn' src='/plugins/visualizations/mda_heatmap_viz/static/images/next.png' alt='Next' onmouseover='userHelpClose();' style="display:none;" onclick='searchNext();'  align="top"  />
+		    <img id='cancel_btn' src='/plugins/visualizations/mda_heatmap_viz/static/images/cancel.png' alt='Cancel' onmouseover='detailDataToolHelp(this,"Clear current search")' style="display:none;" onclick='clearSearch();'  align="top"  />
+		    <img id='pdf_btn' src='/plugins/visualizations/mda_heatmap_viz/static/images/pdf.png' alt='go' onmouseover='detailDataToolHelp(this,"Save as PDF")' onclick='openPdfPrefs(this,null);'  align="top" style="position: absolute; right: 60;"  />
+ 	    	    <img id='gear_btn' src='/plugins/visualizations/mda_heatmap_viz/static/images/gear.png' alt='Modify Map' onmouseover='detailDataToolHelp(this,"Modify Map Preferences")' onclick='editPreferences(this,null);' align="top" style="position: absolute; right: 30;"  />
+       </div>
+    </div>
+
+    <div id="container">
+
+       <div id='summary_chm' style='position: relative;'>
+          <canvas id='summary_canvas'></canvas>
+		<div id='sumlabelDiv' style="display: inline-block"></div>
+       </div>
+
+	  <div id= 'divider' style='position: relative;' onmousedown="dividerStart()" ontouchstart="dividerStart()">
+	  </div>
+
+       <div id='detail_chm' style='position: relative;'>
+          <canvas id='detail_canvas' style='display: inline-block'></canvas>
+          <div id='labelDiv' style="display: inline-block"></div>
+       </div>
+   </div>
+
+	<div id="pdfPrefsPanel" style="display: none; position: absolute; background-color: rgb(203, 219, 246);">
+		<div class="prefsHeader" id="pdfPrefsHeader">PDF Generation</div>
+		<table>
+			<tbody>
+				<tr>
+					<td>
+						<div id="pdfprefprefs" style="display: block; background-color: rgb(203, 219, 246);">
+							<div style="display: inherit; width: 220px; height: 220px;">
+								<h3 style="margin-bottom:0px;">Show maps:</h3>
+								<input id="pdfInputSummaryMap" type="radio" name="pages" value="summary"> Summary<br>
+								<input id="pdfInputDetailMap" type="radio" name="pages" value="detail"> Detail<br>
+								<input id="pdfInputBothMaps" type="radio" name="pages" value="both" checked> Both<br><br>
+								<input id="pdfInputPages" type="checkbox" name="pages" value="separate"> Show maps on separate pages<br>							
+								<input id="pdfInputPortrait" type="radio" name="orientation" value="portrait"> Portrait 
+								<input id="pdfInputLandscape" type="radio" name="orientation" value="Landscape" checked> Landscape <br>							
+								<h3 style="margin-bottom:0px;">Show classification bars:</h3>							
+								<input id="pdfInputCondensed" type="radio" name="condensed" value="condensed"> Condensed 
+								<input id="pdfInputHistogram" type="radio" name="condensed" value="histogram" checked> Histogram <br>							
+								<input id="pdfInputColumn" type="checkbox" name="class" value="row" checked> Column<br>							
+								<input id="pdfInputRow" type="checkbox" name="class" value="column" checked> Row
+							</div>
+							<table>
+								<tbody>
+									<tr>
+										<td style="font-weight: bold;">
+											<div id="pref_buttons" align="right">
+												<img id="prefCancel_btn" src="/plugins/visualizations/mda_heatmap_viz/static/images/prefCancel.png" alt="Cancel changes" onclick="pdfCancelButton();" align="top">&nbsp;&nbsp;
+												<img id="prefCreate_btn" src="/plugins/visualizations/mda_heatmap_viz/static/images/createPdf.png" alt="Create PDF" onclick="getPDF();" align="top">
+											</div>
+										</td>
+									</tr>
+								</tbody>
+							</table>
+						</div>
+					</td>
+				</tr>
+			</tbody>
+		</table>
+	</div>
+</BODY >
+</HTML>
\ No newline at end of file