Index: /issm/trunk-jpl/src/c/bamg/BamgGeom.h
===================================================================
--- /issm/trunk-jpl/src/c/bamg/BamgGeom.h	(revision 22893)
+++ /issm/trunk-jpl/src/c/bamg/BamgGeom.h	(revision 22894)
@@ -7,4 +7,7 @@
 class BamgGeom{
 
+
+// HI DANIEL I'M DANIEL.
+//FLATTEN THAT ARRAY AND SET THAT SIZE. THANKS.
 	public:
 		int     VerticesSize[2];
Index: /issm/trunk-jpl/src/m/array/arrayoperations.js
===================================================================
--- /issm/trunk-jpl/src/m/array/arrayoperations.js	(revision 22893)
+++ /issm/trunk-jpl/src/m/array/arrayoperations.js	(revision 22894)
@@ -258,4 +258,10 @@
 	return notarray;
 } //}}}
+function ArrayFlip(array) { //{{{
+	
+	var notarray=array.slice();
+	for (var i=0;i<array.length;i++)notarray[i]=array[i]^1;
+	return notarray;
+} //}}}
 function ArrayCopy(array) { //{{{
 
@@ -293,5 +299,5 @@
         for(var i=0;i<array.length;i++){
             for(var j=0;j<array[0].length;j++){
-                if (Number.isNaN(array[i][j])) return 1;
+                if (isNaN(array[i][j])) return 1;
             }
         }
@@ -299,5 +305,5 @@
     else{
         for(var i=0;i<array.length;i++){
-            if (Number.isNaN(array[i])) return 1;
+            if (isNaN(array[i])) return 1;
         }
     }
@@ -333,5 +339,5 @@
 function ArrayAnyEqual(array,value) { //{{{
 	
-	if(!Number.isNaN(value)){
+	if(!isNaN(value)){
 		for(var i=0;i<array.length;i++){
 			if (array[i]==value)return 1;
@@ -340,5 +346,5 @@
 	else{
 		for(var i=0;i<array.length;i++){
-			if (Number.isNaN(array[i]))return 1;
+			if (isNaN(array[i]))return 1;
 		}
 	}
@@ -553,2 +559,45 @@
 
 } //}}}
+function typedArraySliceSupport() { //{{{
+	//TypedArray compatibility for Safari/IE
+	if (typeof Int8Array !== 'undefined') {
+		if (!Int8Array.prototype.fill) { Int8Array.prototype.fill = Array.prototype.fill; }
+		if (!Int8Array.prototype.slice) { Int8Array.prototype.slice = Array.prototype.slice; }
+	}
+	if (typeof Uint8Array !== 'undefined') {
+		if (!Uint8Array.prototype.fill) { Uint8Array.prototype.fill = Array.prototype.fill; }
+		if (!Uint8Array.prototype.slice) { Uint8Array.prototype.slice = Array.prototype.slice; }
+	}
+	if (typeof Uint8ClampedArray !== 'undefined') {
+		if (!Uint8ClampedArray.prototype.fill) { Uint8ClampedArray.prototype.fill = Array.prototype.fill; }
+		if (!Uint8ClampedArray.prototype.slice) { Uint8ClampedArray.prototype.slice = Array.prototype.slice; }
+	}
+	if (typeof Int16Array !== 'undefined') {
+		if (!Int16Array.prototype.fill) { Int16Array.prototype.fill = Array.prototype.fill; }
+		if (!Int16Array.prototype.slice) { Int16Array.prototype.slice = Array.prototype.slice; }
+	}
+	if (typeof Uint16Array !== 'undefined') {
+		if (!Uint16Array.prototype.fill) { Uint16Array.prototype.fill = Array.prototype.fill; }
+		if (!Uint16Array.prototype.slice) { Uint16Array.prototype.slice = Array.prototype.slice; }
+	}
+	if (typeof Int32Array !== 'undefined') {
+		if (!Int32Array.prototype.fill) { Int32Array.prototype.fill = Array.prototype.fill; }
+		if (!Int32Array.prototype.slice) { Int32Array.prototype.slice = Array.prototype.slice; }
+	}
+	if (typeof Uint32Array !== 'undefined') {
+		if (!Uint32Array.prototype.fill) { Uint32Array.prototype.fill = Array.prototype.fill; }
+		if (!Uint32Array.prototype.slice) { Uint32Array.prototype.slice = Array.prototype.slice; }
+	}
+	if (typeof Float32Array !== 'undefined') {
+		if (!Float32Array.prototype.fill) { Float32Array.prototype.fill = Array.prototype.fill; }
+		if (!Float32Array.prototype.slice) { Float32Array.prototype.slice = Array.prototype.slice; }
+	}
+	if (typeof Float64Array !== 'undefined') {
+		if (!Float64Array.prototype.fill) { Float64Array.prototype.fill = Array.prototype.fill; }
+		if (!Float64Array.prototype.slice) { Float64Array.prototype.slice = Array.prototype.slice; }
+	}
+	if (typeof TypedArray !== 'undefined') {
+		if (!TypedArray.prototype.fill) { TypedArray.prototype.fill = Array.prototype.fill; }
+		if (!TypedArray.prototype.slice) { TypedArray.prototype.slice = Array.prototype.slice; }
+	}
+} //}}}
Index: /issm/trunk-jpl/src/m/classes/clusters/generic.js
===================================================================
--- /issm/trunk-jpl/src/m/classes/clusters/generic.js	(revision 22893)
+++ /issm/trunk-jpl/src/m/classes/clusters/generic.js	(revision 22894)
@@ -1,174 +1,354 @@
-//GENERIC class definition
-//
-//   Usage:
-//      generic=new generic();
+// Execute script in strict mode
+'use strict';
+/**
+ * generic - Class that allows for sending requests to server and handling response or errors
+ *
+ *	Usage:
+ *		generic = new generic();
+ *
+ *	Todo:
+ *		Convert to ES6 class
+ */
+function generic() {
+	// Retrieve options and apply to properties 
+	// {{{
+	let args 	= Array.prototype.slice.call(arguments);
+	let options = new pairoptions(args.slice(0, args.length));
+	
+	this.url			= options.getfieldvalue('url', '');
+	this.np				= options.getfieldvalue('np', 3);
+	this.codeversion	= options.getfieldvalue('codeversion', 20486);
+	this.codepath		= options.getfieldvalue('codepath', 'issmdir/bin');
+	this.executionpath	= options.getfieldvalue('executionpath', 'issmdir/execution');
+	//}}}
+	
+	
+	// Methods
+	//{{{
+	this.disp = function() { //{{{
+		console.log(sprintf('   generic class echo:'));
+		console.log(sprintf('    url: %s', this.url));
+		console.log(sprintf('    np: %i', this.np));
+		console.log(sprintf('    codepath: %s', this.codepath));
+		console.log(sprintf('    executionpath: %s', this.executionpath));
+	}; //}}}
+	
+	this.classname = function() { //{{{
+		return 'generic';
+	}; //}}}
+	
+	this.checkconsistency = function(md, solution, analyses) { //{{{
+		if (cluster.np < 1) {
+			md.checkmessage('Number of processors should be at least 1!');
+		}
+		if (isNaN(cluster.np)) {
+			md.checkmessage('Number of processors NaN!');
+		}
+	}; //}}}
+	
+	this.BuildQueueScript = function(cluster, dirname, modelname, solution, io_gather, isvalgrind, isgprof, isdakota) { //{{{
+		//write queuing script 
+		//what is the executable being called? 
+		executable 	= 'issm.exe';
+		
+		fid 		= fopen(modelname + '.queue','w');
+		fprintf(fid, '#!%s\n', cluster.shell);
+		fprintf(fid, 'mpiexec -np %i %s/%s %s %s %s 2> %s.errlog >%s.outlog ', cluster.np, cluster.codepath, executable, EnumToString(solution), cluster.executionpath + '/' + dirname, modelname, modelname, modelname);					
+		fclose(fid);
+	}; //}}}
+	
+	this.UploadAndRun = function(md, fid, toolkitsstring, solutionstring, name, runtimename, successCallback, errorCallback, solveButtonId, callout, withProgressBar) { //{{{
+		/* Local constants */
+		let PROGRESS_BAR_ID 				= 'solve-progress-bar';
+		let PROGRESS_BAR_TEXT_PERCENTAGE_ID = 'progress-bar-text-percentage';		
+		
+		/* Local variables */
+		let hasCallout 					= false;
+		let isProgressBarLoaded			= false;
+		let progressBar					= {};
+		let progressBarTextPercentage 	= {};
+		let request 					= {};
+		let solveButton 				= $(solveButtonId);
+		let solveButtonText 			= !vesl.helpers.isEmptyOrUndefined(solveButton) ? solveButton.text() : ''; // Save initial solve button text
+		
+		/* Local functions */
+		// NOTE: After conversion of generic to ES6 class, these should be class methods
+		function loadProgressBar() {
+			callout.setContent('\
+				<div class="progress-bar-wrapper">\
+					<progress id="' + PROGRESS_BAR_ID + '" value="0" max="100"></progress>\
+					<div class="progress-bar-text">\
+						<span id="' + PROGRESS_BAR_TEXT_PERCENTAGE_ID + '">0</span>\
+						<span>%</span>\
+					</div>\
+				</div>\
+			');
+			
+			progressBar 				= $('#' + PROGRESS_BAR_ID);
+			progressBarTextPercentage 	= $('#' + PROGRESS_BAR_TEXT_PERCENTAGE_ID);
+			isProgressBarLoaded 		= true;
+		}
+		
+		function setProgressBar(progress) {
+			progressBar.val(progress);
+			progressBarTextPercentage.text(progress);
+		}
+		
+		// Check certain arguments
+		hasCallout = !vesl.helpers.isEmptyOrUndefined(callout);		
+		
+		// Check that we have a connection
+		if (!navigator.onLine) {
+			console.log('Error: no connection!');
+			if (hasCallout) {
+				callout.set('No connection!', '');
+			} else {
+				solveButton.text('No connection!').prop('disabled', false);
+			}
+			errorCallback();
+			return;
+		}
+		
+		// Create request
+		request = new XMLHttpRequest();
+		request.open('POST', this.url, true);
+		
+		// Set request properties
+		request.position 	= 0; // Keep track of current parsing position in repsonseText
+		request.timeout 	= 1000 * 60 * 60 * 3; // 3 hrs (in milliseconds); NOTE: This should match FastCgiServer timeout in Apache conf
+		
+		request.ontimeout = function(event) { //{{{
+			console.log('Error: timeout!');
+			
+			if (hasCallout) {
+				callout.set(vesl.ERROR_HEADER_GENERAL, '<p>Request timeout! ' + vesl.ERROR_CONTENT_TRY_AGAIN + '</p>');
+			} else {
+				solveButton.text('Timeout!').prop('disabled', false);
+			}
+			
+			errorCallback();
+		}; //}}}
+		
+		request.onerror = function(event) { //{{{
+			console.log('Error: could not run!');
+			
+			if (hasCallout) {
+				callout.set(vesl.ERROR_HEADER_GENERAL, '<p>Something went wrong. ' + vesl.ERROR_CONTENT_TRY_AGAIN + '</p>');
+			} else {
+				solveButton.text('Could not run!').prop('disabled', false);
+			}
+			
+			errorCallback();
+		}; //}}}
+		
+		request.upload.onprogress = function(event) { //{{{
+			let progress = (event.loaded / event.total * 100).toFixed(0);
+			
+			if (hasCallout) {
+				callout.setHeader('Sending request...');
+				if (withProgressBar) {
+					if (!isProgressBarLoaded) {
+						loadProgressBar();
+					}
+					setProgressBar(progress);
+				} else {
+					callout.setContent('<p>' + progress + '%</p>');
+				}
+			} else {
+				solveButton.text('Sending: ' + progress + '%');
+			}
+        }; //}}}
+        
+		request.onprogress = function(event) { //{{{
+			/* Local variables */
+			let progress 	= 0;
 
-function generic (){
-	//properties 
-	// {{{
-	var args = Array.prototype.slice.call(arguments);
-	var options = new pairoptions(args.slice(0,args.length));
+			// Receive updates by parsing message length as a 32-bit hex string of form 0x*09ABCDEF))
+			let startIndex 	= request.position;
+			let endIndex 	= request.position + 10;
+			
+			if (request.responseText.length >= endIndex) { // Ensure entire hex string is loaded
+				let chunkSize = parseInt(request.responseText.slice(startIndex, endIndex));
+				
+				startIndex 	= endIndex;
+				endIndex 	= startIndex + chunkSize;
+				
+				if (chunkSize >= 1024) { // Arbitrary maximium size of message (Must be below minimium size of model results)
+					progress = ((request.responseText.length - request.position) / chunkSize * 100).toFixed(0);
+					if (hasCallout) {
+						callout.setHeader('Downloading result...');
+						if (withProgressBar) {
+							setProgressBar(progress);
+						} else {
+							callout.setContent('<p>' + progress + '%</p>');
+						}
+					} else {
+						solveButton.text('Downloading: ' + progress + '%').prop('disabled', true);
+					}
+				} else if (request.responseText.length >= endIndex) { // Ensure entire chunk is loaded
+					progress = parseInt(request.responseText.slice(startIndex, endIndex));
+					if (hasCallout) {
+						callout.setHeader('Computing...');
+						if (withProgressBar) {
+							setProgressBar(progress);
+						} else {
+							callout.setContent('<p>' + progress + '%</p>');
+						}
+					} else {
+						solveButton.text('Computing: ' + progress + '%').prop('disabled', true);
+					}
+					request.position = endIndex;
+				}
+			}
+		}; //}}}
+		
+		request.onload = function(event) { //{{{
+			/* Local variables */
+			//{{{
+			let buffer 				= {};
+// 			let bufferInflated		= {};
+			let responseText 		= '';
+			//}}}
+			
+			// TODO: Get context to this.str2ab or otherwise declare it in order to avoid duplication
+			function str2ab(str) { //{{{
+				let buffer = new Uint8Array(str.length);
+				for (let i = 0, strLen = str.length; i < strLen; ++i) {
+					buffer[i] = str.charCodeAt(i);
+				}
+				return buffer;
+			} //}}}
+			
+			try {
+/*
+				console.log(request.responseText);
+				console.log('request.position : ' + request.position);
+				console.log('request.responseType : ' + request.responseType);
+				console.log('request.responseText.length : ' + request.responseText.length);
+*/
+				responseText 	= window.atob(request.responseText.slice(request.position + 10).replace(/\s/g, ''));
+// 				console.log('responseText.length : ' + responseText.length);
+				buffer 			= str2ab(responseText);
+// 				console.log('buffer.length : ' + buffer.length);
+/*
+	            bufferInflated 	= UZIP.inflate(buffer);
+				console.log('bufferInflated.length : ' + bufferInflated.length);
+*/
+	            
+/*
+				returnBuffer 		= new Uint8Array(buffer);
+				returnBufferSize 	= returnBuffer.byteLength;
+*/
+			
+				//Write result buffer to file for debugging. Filename and MIME type are optional.
+				//writetofile(returnBuffer, 'resultBuffer', 'application/octet-stream');
+// 				md.results = parseresultsfrombuffer(md, bufferInflated, bufferInflated.length);
+				md.results = parseresultsfrombuffer(md, buffer, buffer.length);
+				
+				// Let front end script handle changes to callout
+				if (hasCallout) {
+					callout.set('Success!', '<p>Solve successful</p>');
+				} else {
+					solveButton.text(solveButtonText).prop('disabled', false);
+				}
+				successCallback();
+			} catch (e) {
+				console.log(e);
+				if (vesl.string.startsWith(responseText, 'Error')) {
+					if (hasCallout) {
+						callout.set(vesl.ERROR_HEADER_GENERAL, '<p>Something went wrong. ' + vesl.ERROR_CONTENT_TRY_AGAIN + '</p>');
+					} else {
+						solveButton.text('ISSM error!').prop('disabled', false);
+					}
+				} else {
+					if (hasCallout) {
+						callout.set(vesl.ERROR_HEADER_GENERAL, '<p>Something went wrong. ' + vesl.ERROR_CONTENT_TRY_AGAIN + '</p>');
+					} else {
+						solveButton.text('JS error!').prop('disabled', false);
+					}
+				}
+				errorCallback();
+			}
+		}; //}}}
+		
+		request.responseType = 'application/octet-stream';
+		
+		/* Construct request */
+		let npbuffer = this.str2ab(md.cluster.np.toString());
+		npbuffer = UZIP.deflate(npbuffer);
+		let nplength = new Uint32Array(1);
+		nplength[0] = npbuffer.byteLength;
+		
+		let codeversionbuffer = this.str2ab(md.cluster.codeversion.toString());
+		codeversionbuffer = UZIP.deflate(codeversionbuffer);
+		let codeversionlength = new Uint32Array(1);
+		codeversionlength[0] = codeversionbuffer.byteLength;
+		
+		let runtimenamebuffer = this.str2ab(runtimename);
+		runtimenamebuffer = UZIP.deflate(runtimenamebuffer);
+		let runtimenamelength = new Uint32Array(1);
+		runtimenamelength[0] = runtimenamebuffer.byteLength;
+		
+		let namebuffer = this.str2ab(name);
+		namebuffer = UZIP.deflate(namebuffer);
+		let namelength = new Uint32Array(1);
+		namelength[0] = namebuffer.byteLength;
+		
+		let toolkitsbuffer = this.str2ab(toolkitsstring);
+		toolkitsbuffer = UZIP.deflate(toolkitsbuffer);
+		let toolkitslength = new Uint32Array(1);
+		toolkitslength[0] = toolkitsbuffer.byteLength;
+		
+		let solutionbuffer = this.str2ab(solutionstring);
+		solutionbuffer = UZIP.deflate(solutionbuffer);
+		let solutionlength = new Uint32Array(1);
+		solutionlength[0] = solutionbuffer.byteLength;
+		
+		let binbuffer = new Uint8Array(fid.rawbuffer()); //seems that 16 bits length could be incompatible.
+		binbuffer = UZIP.deflate(binbuffer);
+		let binlength = new Uint32Array(1);
+		binlength[0] = binbuffer.byteLength;
+		
+		let data = new Blob(
+			[
+				nplength,
+				npbuffer,
+				codeversionlength,
+				codeversionbuffer,
+				runtimenamelength,
+				runtimenamebuffer,
+				namelength,
+				namebuffer,
+				toolkitslength,
+				toolkitsbuffer,
+				solutionlength,
+				solutionbuffer,
+				binlength,
+				binbuffer
+			]
+		);
 
-	this.url=options.getfieldvalue('url','');
-	this.np=options.getfieldvalue('np',3);
-	this.codeversion=options.getfieldvalue('codeversion',20486);
-	this.codepath=options.getfieldvalue('codepath','issmdir/bin');
-	this.executionpath=options.getfieldvalue('executionpath','issmdir/execution');
-	//}}}
-	//methods
-	this.disp= function(){// {{{
-		console.log(sprintf('   generic class echo:'));
-		console.log(sprintf('    url: "%s"',this.url));
-		console.log(sprintf('    np: %i',this.np));
-		console.log(sprintf('    codepath: "%s"',this.codepath));
-		console.log(sprintf('    executionpath: "%s"',this.executionpath));
-	}// }}}
-	this.classname= function(){// {{{
-		return "generic";
-	}// }}}
-	this.checkconsistency = function (md,solution,analyses) { //{{{
-		if (cluster.np<1){
-			md.checkmessage('number of processors should be at least 1');
-		}
-		if (isNaN(cluster.np)){
-			md.checkmessage('number of processors should not be NaN!');
-		}
-	} //}}}
-	this.BuildQueueScript = function (cluster,dirname,modelname,solution,io_gather,isvalgrind,isgprof,isdakota) { // {{{
-
-			//write queuing script 
-			//what is the executable being called? 
-			executable='issm.exe';
-
-			fid=fopen(modelname+'.queue','w');
-			fprintf(fid,'#!%s\n',cluster.shell);
-			fprintf(fid,'mpiexec -np %i %s/%s %s %s %s 2> %s.errlog >%s.outlog ',cluster.np,cluster.codepath,executable,EnumToString(solution),cluster.executionpath+'/'+dirname,modelname,modelname,modelname);					
-			fclose(fid);
-	} //}}}
-	this.UploadAndRun = function (md,callbackfunction,callbackerrorfunction,callbackid,fid,toolkitsstring,solutionstring,name,runtimename) { //{{{
-		if (!navigator.onLine) { //{{{
-			$(callbackid).html(sprintf("%-16s", "NO CONNECTION")).prop("disabled", false);
-			callbackerrorfunction();
-			return;
-		} //}}}
-		var request = new XMLHttpRequest();
-		request.open("POST", this.url, true);
-		$(callbackid).html(sprintf("%-16s", "CONNECTING...")).prop("disabled", true);
-		request.position = 0; //Keep track of current parsing position in repsonseText
-		request.timeout = 180000;
-		request.ontimeout = function (event) { //{{{
-			$(callbackid).html(sprintf("%-16s", "TIMEOUT")).prop("disabled", false);
-			callbackerrorfunction();
-		} //}}}
-		request.onerror = function (event) { //{{{
-			$(callbackid).html(sprintf("%-16s", "COULD NOT RUN")).prop("disabled", false);
-			callbackerrorfunction();
-		} //}}}
-		request.upload.onprogress = function(event) { //{{{
-			var progress = (event.loaded / event.total * 100).toFixed(0);
-			$(callbackid).html(sprintf("%-20s", "UPLOADING: " + progress + "%"));
-        } //}}}
-		request.onprogress = function (event) { //{{{
-			//Receive updates by parsing message length as a 32-bit hex string of form 0x*09ABCDEF))
-			var startIndex = request.position;
-			var endIndex = request.position + 10;
-			if (request.responseText.length >= endIndex) { //Ensure entire hex string is loaded
-				var chunkSize = parseInt(request.responseText.slice(startIndex, endIndex));
-				startIndex = endIndex;
-				endIndex = startIndex + chunkSize;
-				if (chunkSize >= 1024) { //Arbitrary maximium size of message (Must be below minimium size of model results)
-					$(callbackid).html(sprintf("%-20s", "DOWNLOADING: " + ((request.responseText.length - request.position) / chunkSize * 100).toFixed(0) + "%")).prop("disabled", true);
-				}
-				else if (request.responseText.length >= endIndex) { //Ensure entire chunk is loaded
-					var responseChunk = request.responseText.slice(startIndex, endIndex);
-					$(callbackid).html(responseChunk);
-					request.position = endIndex;
-				}
-			}
-		}; //}}}
-		request.onload = function (event) { //{{{
-			//get context to this.str2ab to avoid duplciation
-			function str2ab(str) {
-				var buf = new Uint8Array(str.length);
-				for (var i=0, strLen=str.length; i < strLen; i++) {
-					buf[i] = str.charCodeAt(i);
-				}
-				return buf;
-			}
-			var responseText = window.atob(request.responseText.slice(request.position + 10).replace(/\s/g, ''));
-            var buffer = pako.inflate(str2ab(responseText));
-			var returnBuffer = new Uint8Array(buffer);
-			var returnBuffer_size = returnBuffer.byteLength;
-			try {
-				//Write result buffer to file for debugging. Filename and MIME type are optional.
-				//writetofile(returnBuffer, "resultBuffer", "application/octet-stream");
-				md.results = parseresultsfrombuffer(md,returnBuffer,returnBuffer_size);
-				$(callbackid).html(sprintf("%-16s", "RUN")).prop("disabled", false);
-				callbackfunction();
-			}
-			catch (e) {
-				if (responseText.startsWith('Error')) {
-					console.log(responseText);
-					$(callbackid).html(sprintf("%-16s", "ISSM ERROR")).prop("disabled", false);
-				}
-				else {
-					$(callbackid).html(sprintf("%-16s", "JS ERROR")).prop("disabled", false);
-					console.log(e);
-				}
-				callbackerrorfunction();
-			}
-			
-		}; //}}}
-		
-		var npbuffer = this.str2ab(md.cluster.np.toString());
-		npbuffer = pako.deflate(npbuffer);
-		var nplength = new Uint32Array(1);
-		nplength[0] = npbuffer.byteLength;
-		
-		var codeversionbuffer = this.str2ab(md.cluster.codeversion.toString());
-		codeversionbuffer = pako.deflate(codeversionbuffer);
-		var codeversionlength = new Uint32Array(1);
-		codeversionlength[0] = codeversionbuffer.byteLength;
-		
-		var runtimenamebuffer = this.str2ab(runtimename);
-		runtimenamebuffer = pako.deflate(runtimenamebuffer);
-		var runtimenamelength = new Uint32Array(1);
-		runtimenamelength[0] = runtimenamebuffer.byteLength;
-		
-		var namebuffer = this.str2ab(name);
-		namebuffer = pako.deflate(namebuffer);
-		var namelength = new Uint32Array(1);
-		namelength[0] = namebuffer.byteLength;
-		
-		var toolkitsbuffer = this.str2ab(toolkitsstring);
-		toolkitsbuffer = pako.deflate(toolkitsbuffer);
-		var toolkitslength = new Uint32Array(1);
-		toolkitslength[0] = toolkitsbuffer.byteLength;
-		
-		var solutionbuffer = this.str2ab(solutionstring);
-		solutionbuffer = pako.deflate(solutionbuffer);
-		var solutionlength = new Uint32Array(1);
-		solutionlength[0] = solutionbuffer.byteLength;
-		
-		var binbuffer = new Uint8Array(fid.rawbuffer()); //seems that 16 bits length could be incompatible.
-		binbuffer = pako.deflate(binbuffer);
-		var binlength = new Uint32Array(1);
-		binlength[0] = binbuffer.byteLength;
-		
-		var data = new Blob([nplength,npbuffer,codeversionlength,codeversionbuffer,runtimenamelength,runtimenamebuffer,namelength,namebuffer,toolkitslength,toolkitsbuffer,solutionlength,solutionbuffer,binlength,binbuffer]);
-		
-		request.responseType = 'application/octet-stream';
+		// Send request
 		request.send(data);
 		
-	} //}}}
+		if (hasCallout) {
+			callout.set('Connecting...', '');
+		} else {
+			solveButton.text('Connecting...').prop('disabled', true);
+		}
+	}; //}}}
+	
 	this.ab2str = function(buf) { //{{{
 		return String.fromCharCode.apply(null, new Uint16Array(buf));
-	} //}}}
+	}; //}}}
+	
 	this.str2ab = function(str) { //{{{
-		var buf = new Uint8Array(str.length);
-		for (var i=0, strLen=str.length; i < strLen; i++) {
+		let buf = new Uint8Array(str.length);
+		
+		for (let i = 0, strLen = str.length; i < strLen; i++) {
 			buf[i] = str.charCodeAt(i);
 		}
+		
 		return buf;
-	} //}}}
+	}; //}}}
 } 
Index: /issm/trunk-jpl/src/m/plot/applyoptions.js
===================================================================
--- /issm/trunk-jpl/src/m/plot/applyoptions.js	(revision 22893)
+++ /issm/trunk-jpl/src/m/plot/applyoptions.js	(revision 22894)
@@ -13,5 +13,4 @@
 			//{{{ Variable options initialization
 			var caxis = options.getfieldvalue('caxis');
-			var colorbarinnerlabels = options.getfieldvalue('colorbarinnerlabels','off');
 			var ccanvasid, ctitleid, clabelsid, ccanvas, ctitle, clabels, ccontext, cmap, colorbar, cwidth, cheight, cgradient, color, y, x;
 			//}}}
@@ -83,12 +82,4 @@
 			clabelsid = options.getfieldvalue('colorbarid', ccanvasid).replace('canvas','labels');
 			clabels = $('#'+clabelsid);
-			if (colorbarinnerlabels=='on') {
-				clabels.removeClass('sim-colorbar-labels-outer');
-				clabels.addClass('sim-colorbar-labels-inner');
-			}
-			else {
-				clabels.removeClass('sim-colorbar-labels-inner');
-				clabels.addClass('sim-colorbar-labels-outer');
-			}
 			var clabelstring = '';
 			clabels.empty();
@@ -137,5 +128,5 @@
 	if (options.exist('maskregion')) {
 		var maskObject = options.getfieldvalue('maskregion',{'enabled':false});
-		if (maskObject.enabled && !VESL.Helpers.isEmptyOrUndefined(maskObject.colors)) {
+		if (maskObject.enabled && !vesl.helpers.isEmptyOrUndefined(maskObject.colors)) {
 			var x = 0;
 			var sections = Object.keys(maskObject.colors).length + 1;
@@ -157,70 +148,19 @@
 	//}}}
 	//{{{ text display
-	var overlaycanvasid = options.getfieldvalue('overlayid', options.getfieldvalue('canvasid')+'-overlay');
-	var overlaycanvas = $('#'+overlaycanvasid)[0];
-	if (!VESL.Helpers.isEmptyOrUndefined(overlaycanvas)) {
+	var ctx;
+	var overlaycanvasid;
+	var overlaycanvas;
+	//Only intialize overlay canvas once by checking if it's already been defined
+	if (vesl.helpers.isEmptyOrUndefined(canvas.overlaycanvas)) {
 		//Get drawing context and save reference on main WebGL canvas
-		var ctx = overlaycanvas.getContext('2d');
+		overlaycanvasid = options.getfieldvalue('overlayid', options.getfieldvalue('canvasid') + '-overlay')
+		overlaycanvas = $('#' + overlaycanvasid)[0];
+		ctx = overlaycanvas.getContext('2d');
 		canvas.overlaycanvas = overlaycanvas;
-		
-		//Resize interal viewport coordinates to match screenspace coordinates
-		var rect = overlaycanvas.getBoundingClientRect();
-		overlaycanvas.width  = rect.width;
-		overlaycanvas.height = rect.height;
-		
-		//Clear canvas each frame for any new drawings
-		canvas.overlayHandlers['draw'] = function(overlaycanvas) {
-			ctx.clearRect(0, 0, overlaycanvas.width, overlaycanvas.height);
-		}
 	}
-	//{{{ lat long overlay
-	if (options.exist('latlongoverlay')) {
-		var latitudes = {
-			//'-90': 1,
-			//'-65': .999,
-			'-60': 0.994046875,
-			'-45': 0.955729166666666,
-			'-30': 0.9226562500000024,
-			//'-15': 0.830729166666665,
-			'0': 0.74218749999999811,
-			//'15': 0.63932291666666663,
-			'30': 0.523390625000001,
-			'45': 0.4020001,
-			'60': 0.26953124999999978,
-			//'65': 0.225390625,
-			//'90': 0.0,
-		}
-		var longitudes = [-150, -120, -90, -60, -30, 0, 30, 60, 90, 120, 150, 180];
-		canvas.overlayHandlers['latlong'] = function(canvas) {
-			//Transform from world space to viewport space
-			var centerx = overlaycanvas.width / 2;
-			var centery = overlaycanvas.height / 2;
-			var radius = (overlaycanvas.height) / 2;
-
-			//Draw latitudes
-			ctx.setLineDash([5, 10]);
-			for(latitude in latitudes) {
-				ctx.beginPath();
-				ctx.arc(centerx, centery, radius * latitudes[latitude], 0, 2 * Math.PI);
-				ctx.stroke();
-				ctx.font = 'bold ' + String(options.getfieldvalue('colorbarfontsize', 18))+'px "Lato", Helvetica, Arial, sans-serif';
-				ctx.fillStyle = options.getfieldvalue('colorbarfontcolor','black');
-				ctx.strokeStyle = options.getfieldvalue('colorbarfontcolor','black');
-				ctx.textAlign = 'center';
-				ctx.textBaseline = 'middle';
-				ctx.fillText(latitude, centerx, centery + radius * latitudes[latitude]);
-				ctx.strokeText(latitude, centerx, centery + radius * latitudes[latitude]);
-			}
-			//Draw longitudes
-			ctx.setLineDash([1, 0]);
-			for (longitude in longitudes) {
-				ctx.beginPath();
-				ctx.moveTo(centerx, centery);
-				ctx.lineTo(centerx + radius * Math.sin(longitudes[longitude] * DEG2RAD), centery + radius * Math.cos(longitudes[longitude] * DEG2RAD));
-				ctx.stroke();
-			}
-		}
-	} //}}}
-	if (options.exist('textlabels')) {
+	overlaycanvas = canvas.overlaycanvas;
+	ctx = overlaycanvas.getContext('2d');
+	
+	if (options.exist('textlabels')) {//{{{
 		//Attatch new overlay handler to display text labels
 		var textLabels = options.getfieldvalue('textlabels',[]);
@@ -239,5 +179,5 @@
 				// function declared in slr-gfm sim-front-end-controller.js
 				// if labels are behind the globe sphere then skip iteartion and do not display them
-				if (VESL.UI.isLabelVisible(textLabel)) {
+				if (vesl.ui.isLabelVisible(textLabel)) {
 					//Transform from world space to viewport space
 					var screenPoint = vec3.transformMat4(vec3.create(), textLabel.position, canvas.camera.vpMatrix);
@@ -256,5 +196,6 @@
 			}
 		}
-	} //}}}
+	}//}}}
+		
 	//{{{ additional rendering nodes
 	if (options.exist('render')) {
@@ -275,4 +216,5 @@
 
 		var renderObjects = options.getfieldvalue('render',{});
+		
 		for (var renderObject in renderObjects) {
 			//Modify renderObejct?
@@ -289,6 +231,8 @@
 				color: defaultFor(object.color, 'black'),					//Diffuse color of object
 				height: defaultFor(object.height, 25000),					//Height of object along y axis, currently for clouds only
-				range: defaultFor(object.range, 120000),						//Range of sz plane to spawn object, currently for clouds only
-				quantity: defaultFor(object.quantity, 15)					//Quantity of objects to display, currently for clouds only
+				range: defaultFor(object.range, 120000),					//Range of sz plane to spawn object, currently for clouds only
+				quantity: defaultFor(object.quantity, 15),					//Quantity of objects to display, currently for clouds only
+				source: defaultFor(object.source, 'NY'),					//Quantity of objects to display, currently for clouds only
+				targets: defaultFor(object.targets, ['NY'])					//Quantity of objects to display, currently for clouds only
 			};
 			if (!object.enabled) { continue; }
@@ -382,5 +326,5 @@
 				node.patch('Vertices', [object.x, object.y, object.z], 'FaceColor', 'none');
 			}
-			if ('city' === renderObject) {
+			if ('city' === renderObject && !vesl.helpers.isEmptyOrUndefined(overlaycanvas)) {
 				//city
 				var mesh = GL.Mesh.sphere({size: object.size});
@@ -436,4 +380,69 @@
 				}
 			}
+			if ('citylines' === renderObject) {
+				//city
+				node = new Node(
+					'canvas', canvas,
+					'options', options,
+					'renderObject', object,
+					'name', 'citylines',
+					'shaderName', 'ColoredDiffuse',
+					'drawMode', gl.LINES,
+					'diffuseColor', object.color,
+					'lineWidth', options.getfieldvalue('linewidth', 1),
+					'scale', [object.scale, object.scale, object.scale],
+					'rotation', [0, 0, 0]
+				);
+				
+				//For each target city, calculate the shortest line across the earth by performing a quaternion slerp.
+				//Treat source and target city as vectors to rotate to from the north pole.
+				//Then, slerp between the two rotations, and generate points across equidistance points on the earth to create the line.
+				var north = vec3.fromValues(0, 1, 0);
+				var source = object.source;
+				var sourceXYZ = vec3.fromValues(xcity[source], zcity[source], -ycity[source]);
+				var radius = vec3.length(sourceXYZ);
+				var lineSteps = 50;
+				var lineX = [];
+				var lineY = [];
+				var lineZ = [];
+				var lineXYZ = vec3.create();
+
+				for (var i = 0; i < object.targets.length; i++) {
+					var target = object.targets[i];
+					var targetXYZ = vec3.fromValues(xcity[target], zcity[target], -ycity[target]);
+					var axis = vec3.cross(vec3.create(), sourceXYZ, targetXYZ);
+					vec3.normalize(axis, axis);		
+					
+					//Get the total angle between the two cities.
+					var sourceXYZAxis = vec3.normalize(vec3.create(), sourceXYZ);
+					var targetXYZAxis = vec3.normalize(vec3.create(), targetXYZ);
+					var dotProduct = vec3.dot(sourceXYZAxis, targetXYZAxis);
+					var totalAngle = Math.acos(dotProduct); //theta = arccos(u . v / (||u|| * ||v||); in this case, ||u|| and ||v|| are 1, since u and v are unit vectors.
+					
+					var lineQuat = quat.create();
+					for (var j = 1; j <= lineSteps; j++) {
+						//Calculate the partial rotation to obtain points on the line between the two cities.
+						var angle = j / lineSteps * totalAngle;
+						quat.setAxisAngle(lineQuat, axis, angle);
+						quat.normalize(lineQuat, lineQuat); 
+						vec3.transformQuat(lineXYZ, sourceXYZ, lineQuat);
+						//GL.LINES needs 2 points for each line - at the beginning, just use the sourceXYZ.
+						//TODO: Eliminate this if statement.
+						if (j === 1) {
+							lineX.push(sourceXYZ[0]);
+							lineY.push(sourceXYZ[1]);
+							lineZ.push(sourceXYZ[2]);
+						} else {
+							lineX.push(lineX[lineX.length - 1]);
+							lineY.push(lineY[lineY.length - 1]);
+							lineZ.push(lineZ[lineZ.length - 1]);
+						}
+						lineX.push(lineXYZ[0]);
+						lineY.push(lineXYZ[1]);
+						lineZ.push(lineXYZ[2]);
+					}
+				}
+				node.patch('Vertices', [lineX, lineY, lineZ]);
+			}
 		}
 	} //}}}
Index: /issm/trunk-jpl/src/m/plot/plot_mesh.js
===================================================================
--- /issm/trunk-jpl/src/m/plot/plot_mesh.js	(revision 22893)
+++ /issm/trunk-jpl/src/m/plot/plot_mesh.js	(revision 22894)
@@ -7,7 +7,13 @@
 	//   See also: PLOTMODEL, PLOT_MANAGER
 
-	//if ('mesh' in  canvas.nodes && options.getfieldvalue('cachenodes','on') === 'on') return;
+	// If we already have the overlay and are using caching, short circuit
+	if ('mesh' in canvas.nodes && options.getfieldvalue('cachenodes', 'off') === 'on') {
+		return;
+	}
 	
-	//{{{ declare variables:
+	/*
+		Local variables
+	*/
+	//{{{
 	//Process data and model
 	var meshresults = processmesh(md, [], options);
Index: /issm/trunk-jpl/src/m/plot/plot_overlay.js
===================================================================
--- /issm/trunk-jpl/src/m/plot/plot_overlay.js	(revision 22893)
+++ /issm/trunk-jpl/src/m/plot/plot_overlay.js	(revision 22894)
@@ -7,7 +7,13 @@
 	//   See also: PLOTMODEL, PLOT_MANAGER
 
-	if ('overlay' in  canvas.nodes && options.getfieldvalue('cachenodes','on') === 'on') return;
+	// If we already have the overlay and are using caching, short circuit
+	if ('overlay' in canvas.nodes && options.getfieldvalue('cachenodes', 'off') === 'on') {
+		return;
+	}
 	
-	//{{{ declare variables:
+	/*
+		Local variables
+	*/
+	//{{{
 	//Process data and model
 	var meshresults = processmesh(md, [], options);
Index: /issm/trunk-jpl/src/m/plot/plot_quiver.js
===================================================================
--- /issm/trunk-jpl/src/m/plot/plot_quiver.js	(revision 22893)
+++ /issm/trunk-jpl/src/m/plot/plot_quiver.js	(revision 22894)
@@ -10,5 +10,5 @@
 	return;
 	
-	if ('quiver' in  canvas.nodes && noCacheNodesOverride && options.getfieldvalue('cachenodes','on') === 'on') return;
+	//if ('quiver' in canvas.nodes && noCacheNodesOverride && options.getfieldvalue('cachenodes','off') === 'on') return; 
 	
 	//{{{ declare variables:
@@ -29,7 +29,7 @@
 
 	//Only displaying velocity fields for now
-	var v = VESL.Helpers.isEmptyOrUndefined(md.results) ?  md.initialization.vel : md.results[canvas.animation.frame].Vel;
-	var vx = VESL.Helpers.isEmptyOrUndefined(md.results) ? md.initialization.vx : md.results[canvas.animation.frame].Vx;
-	var vy = VESL.Helpers.isEmptyOrUndefined(md.results) ? md.initialization.vy : md.results[canvas.animation.frame].Vy;
+	var v = vesl.helpers.isEmptyOrUndefined(md.results) ?  md.initialization.vel : md.results[canvas.animation.frame].Vel;
+	var vx = vesl.helpers.isEmptyOrUndefined(md.results) ? md.initialization.vx : md.results[canvas.animation.frame].Vx;
+	var vy = vesl.helpers.isEmptyOrUndefined(md.results) ? md.initialization.vy : md.results[canvas.animation.frame].Vy;
 
 	//Handle heightscale
Index: /issm/trunk-jpl/src/m/plot/plot_transient_movie.js
===================================================================
--- /issm/trunk-jpl/src/m/plot/plot_transient_movie.js	(revision 22893)
+++ /issm/trunk-jpl/src/m/plot/plot_transient_movie.js	(revision 22894)
@@ -81,6 +81,6 @@
 			node.updateBuffer('Coords', processedData[frame]);
 			canvas.unitData = processedData[frame];
-			if (canvas.dataMarkers.enabled) {
-				updateMarker(canvas, false);
+			if (canvas.graph.enabled) {
+				vesl.graph.draw(canvas);
 			}
 			if (canvas.playbackSlider) {
@@ -90,5 +90,5 @@
 				canvas.playbackTextProgress.html(steps[frame].toFixed(0) + " " + options.getfieldvalue("movietimeunit","yr"));
 			}
-			if (!VESL.Helpers.isEmptyOrUndefined(canvas.nodes.quiver)) {
+			if (!vesl.helpers.isEmptyOrUndefined(canvas.nodes.quiver)) {
 				plot_quiver(md,options,canvas,false);
 			}
Index: /issm/trunk-jpl/src/m/plot/plot_unit.js
===================================================================
--- /issm/trunk-jpl/src/m/plot/plot_unit.js	(revision 22893)
+++ /issm/trunk-jpl/src/m/plot/plot_unit.js	(revision 22894)
@@ -11,5 +11,5 @@
 		if (options.getfieldvalue('clf','on')=='on') {
 			for (var node in canvas.nodes) {
-				if (node.startsWith('unit')) {
+				if (vesl.string.startsWith(node, 'unit')) {
 					delete canvas.octrees[node];
 					delete canvas.nodes[node];
@@ -31,5 +31,10 @@
 	var is2d = meshresults[4]; 
 	var isplanet = meshresults[5];
-	if (md.mesh.classname() !== 'mesh3dsurface') z = md.geometry.surface;
+	if (md.mesh.classname() !== 'mesh3dsurface') {
+		if (vesl.helpers.isNaN(md.geometry.surface) || (md.geometry.surface[0] !== undefined && vesl.helpers.isNaN(md.geometry.surface[0]))) {
+			md.geometry.surface = NewArrayFill(x.length, 0);
+		}
+		z = md.geometry.surface;
+	}
 	
 	//Compute coordinates and data range:
@@ -48,5 +53,5 @@
 		scale = [1, 1, 1];
 	}
-	
+
 	//Compute gl variables:
 	var edgecolor = options.getfieldvalue('edgecolor', [1.0, 1.0, 1.0 ,1.0]);
Index: /issm/trunk-jpl/src/m/plot/plotdoc.js
===================================================================
--- /issm/trunk-jpl/src/m/plot/plotdoc.js	(revision 22893)
+++ /issm/trunk-jpl/src/m/plot/plotdoc.js	(revision 22894)
@@ -37,7 +37,7 @@
 	console.log('       "colormap": same as standard matlab option (default "jet", ex: "hsv","cool","spring","gray","Ala","Rignot",...)');
 	console.log('       "controlsensitivity": sensitivty of view/zoom changes as a percentage of default (default 1, ex: 0.5, 2.75)');
-	console.log('       "datamarkers": object cotaining data marker parameters. See webgl.js for defaults. (ex: {"enabled":true,"format":["<div id="sim-plot"></div>"],"labels":["thickness","velocity","value"],"animated":true})');
+	console.log('       "dataMarker": object cotaining data marker parameters. See webgl.js for defaults. (ex: {"enabled":true,"format":["<div id="sim-plot"></div>"],"labels":["thickness","velocity","value"],"animated":true})');
 	console.log('       	"enabled": toggle data marker displays (default true, ex: false)');
-	console.log('       	"image": image used for marking the clicked point (ex: "/canvas/data-markers/data_marker.svg")');
+	console.log('       	"image": image used for marking the clicked point (ex: "/canvas/data-markers/data-marker.svg")');
 	console.log('       	"labels": when displaying a sim-plot graph, display these model fields. (ex: ["thickness","velocity","value"])');
 	console.log('       	"font": font to be used for display (ex: "24px "Comic Sans MS", cursive")');
Index: /issm/trunk-jpl/src/m/plot/plotmodel.js
===================================================================
--- /issm/trunk-jpl/src/m/plot/plotmodel.js	(revision 22893)
+++ /issm/trunk-jpl/src/m/plot/plotmodel.js	(revision 22894)
@@ -1,10 +1,8 @@
 function plotmodel(md){ //{{{
-
 	//Convert arguments to array: 
 	var args = Array.prototype.slice.call(arguments);
 
 	//First process options
-	var  options = new plotoptions(args.slice(1,args.length));
-
+	var options = new plotoptions(args.slice(1,args.length));
 	
 	//get number of subplots
@@ -30,5 +28,5 @@
 	
 	//check that nlines and ncols were given at the same time!
-	if ((options.list[0].exist('ncols') & !options.list[0].exist('nlines')) | (options.list[0].exist('nlines') & !options.list[0].exist('ncols'))) throw Error('plotmodel error message: nlines and ncols  need to be specified together, or not at all');
+	if ((options.list[0].exist('ncols') & !options.list[0].exist('nlines')) | (options.list[0].exist('nlines') & !options.list[0].exist('ncols'))) throw Error('plotmodel error message: nlines and ncols need to be specified together, or not at all');
 
 	//go through subplots
Index: /issm/trunk-jpl/src/m/plot/webgl.js
===================================================================
--- /issm/trunk-jpl/src/m/plot/webgl.js	(revision 22893)
+++ /issm/trunk-jpl/src/m/plot/webgl.js	(revision 22894)
@@ -4,15 +4,16 @@
 	//Initialize open Gl for each canvas and clear any previous animation handlers, once per plotmodel call:
 	canvas = document.getElementById(options.getfieldvalue('canvasid'));
-	//var canvas = document.getElementById(options.getfieldvalue('canvasid'));
+	
 	if (!canvas.initialized) {
-		if (!VESL.Helpers.isEmptyOrUndefined(canvas.draw) && canvas.draw.handler !== 0)	{ window.cancelAnimationFrame(canvas.draw.handler); }
-		if (!VESL.Helpers.isEmptyOrUndefined(canvas.animation) && canvas.animation.handler !== 0) { clearInterval(canvas.animation.handler); }
+		if (!vesl.helpers.isEmptyOrUndefined(canvas.draw) && canvas.draw.handler !== 0)	{ window.cancelAnimationFrame(canvas.draw.handler); }
+		if (!vesl.helpers.isEmptyOrUndefined(canvas.animation) && canvas.animation.handler !== 0) { clearInterval(canvas.animation.handler); }
 		initWebGL(canvas, options);
-		draw(canvas);
+		drawCanvas(canvas);
+		
 		canvas.initialized = true;
 		
-		//The onStart event triggers once per plotmodel call load after WebGL and canvas initialization are complete
-		canvas.selector.trigger('onStart', [canvas]);
-	}
+		triggerStartEvent(canvas);
+	}
+	
 	return canvas;
 }
@@ -20,5 +21,6 @@
 	//Initialize canvas.gl on page load, reusing gl context on additional runs
 	var gl = canvas.gl;
-	if (VESL.Helpers.isEmptyOrUndefined(gl)) {
+	
+	if (vesl.helpers.isEmptyOrUndefined(gl)) {
 		gl = GL.create({canvas: canvas});
 		gl.enable(gl.DEPTH_TEST); // Enable depth testing
@@ -36,9 +38,9 @@
 		mc.add(new Hammer.Pan({threshold: 0, pointers: 0}));
 		mc.add(new Hammer.Pinch({threshold: 0})).recognizeWith(mc.get('pan'));
-		mc.on('tap', function (ev) {onTap(ev, canvas);});
-		mc.on('panstart panmove', function (ev) {onPan(ev, canvas, displayview);});
-		mc.on('pinchstart pinchmove', function (ev) {onPinch(ev, canvas, displayview);});
-		canvas.addEventListener('mousewheel', function (ev) {onZoom(ev, canvas, displayzoom)}, false);
-		canvas.addEventListener('DOMMouseScroll', function (ev) {onZoom(ev, canvas, displayzoom)}, false);
+		mc.on('tap', function(ev) {onTap(ev, canvas);});
+		mc.on('panstart panmove', function(ev) {onPan(ev, canvas, displayview);});
+		mc.on('pinchstart pinchmove', function(ev) {onPinch(ev, canvas, displayview);});
+		canvas.addEventListener('mousewheel', function(ev) {onZoom(ev, canvas, displayzoom)}, false);
+		canvas.addEventListener('DOMMouseScroll', function(ev) {onZoom(ev, canvas, displayzoom)}, false);
 		
 		//Add persistent state variables
@@ -60,12 +62,22 @@
 	}
 	
-	if (options.getfieldvalue('clf','on')=='on') {
-		//Add context state variables
-		canvas.render = options.getfieldvalue('render', {});
-		canvas.controlSensitivity = options.getfieldvalue('controlsensitivity', 1);
-		if (options.getfieldvalue('clf','on')=='on') canvas.overlayHandlers = {};
+	if (options.getfieldvalue('clf', 'on') === 'on') {
+		// Add context state variables
+		canvas.render 				= options.getfieldvalue('render', {});
+		canvas.controlSensitivity 	= options.getfieldvalue('controlsensitivity', 1);
+		canvas.overlayHandlers 		= {};
+		
 		var backgroundcolor = new RGBColor(options.getfieldvalue('backgroundcolor', 'lightcyan'));
-		if (backgroundcolor.ok) { canvas.backgroundcolor = [backgroundcolor.r/255.0, backgroundcolor.g/255.0, backgroundcolor.b/255.0, 1.0]; }
-		else { throw Error(sprintf('s%s%s\n','initWebGL error message: cound not find out background color for current canvas ', canvas)); }
+		
+		if (backgroundcolor.ok) { 
+			canvas.backgroundcolor = 	[
+											backgroundcolor.r / 255.0, 
+											backgroundcolor.g / 255.0, 
+											backgroundcolor.b / 255.0, 
+											1.0
+										]; 
+		} else { 
+			throw Error(sprintf('s%s%s\n','initWebGL error message: cound not find out background color for current canvas ', canvas)); 
+		}
 		
 		//Property intiialization, using values from options first, then from default values.
@@ -125,16 +137,27 @@
 			ready: defaultFor(camera.ready, 							false)
 		};
-		var dataMarkers = options.getfieldvalue('datamarkers', {});
-		canvas.dataMarkers = {
-			enabled: defaultFor(dataMarkers.enabled, 					true),
-			values: defaultFor(dataMarkers.values, 						[]),
-			image: defaultFor(dataMarkers.image, 						canvas.assetsPath + '/data-markers/data_marker.svg'),
-			size: defaultFor(dataMarkers.size, 							[32, 32]),
-			format: defaultFor(dataMarkers.format, 						['X: %.2em<br>Y: %.2em<br>Z: %.2em<br>Value: %0.1f']),
-			animated: defaultFor(dataMarkers.animated, 					false),
-			labels: defaultFor(dataMarkers.labels, 						['x', 'y', 'z', 'value']),
-			font: defaultFor(dataMarkers.font, 							''),
-			marker: defaultFor(dataMarkers.marker, 						document.getElementById('sim-data-marker-' + canvas.id)),
-			reposition: defaultFor(dataMarkers.reposition, 				true)
+		if (vesl.helpers.isEmptyOrUndefined(canvas.dataMarker)) {
+			var dataMarker = options.getfieldvalue('dataMarker', {});
+			
+			canvas.dataMarker = {
+				enabled: defaultFor(dataMarker.enabled, 				false),
+				element: defaultFor(dataMarker.element, 				$('#sim-data-marker-' + canvas.id)),
+				isTooltip: defaultFor(dataMarker.isTooltip,				false),
+				handlersReady: defaultFor(dataMarker.handlersReady,		false),
+				initialized: defaultFor(dataMarker.initialized,			false),
+				visible: defaultFor(dataMarker.visible,					false),
+				image: defaultFor(dataMarker.image, 					canvas.assetsPath + '/data-markers/data-marker.svg'),
+				size: defaultFor(dataMarker.size, 						[32, 32]),
+				font: defaultFor(dataMarker.font, 						''),
+				reposition: defaultFor(dataMarker.reposition, 			true)
+			};
+		}
+		var graph = options.getfieldvalue('graph', {});
+		canvas.graph = {
+			enabled: defaultFor(graph.enabled,							false),
+			id: defaultFor(graph.id,									'sim-graph'),
+			animated: defaultFor(graph.animated, 						false),
+			data: defaultFor(graph.data,								[]),
+			otherData: defaultFor(graph.otherData, 						{})
 		};
 		var draw = options.getfieldvalue('draw', {});
@@ -159,5 +182,5 @@
 
 		// Override with parameters from URL, if any
-		VESL.UI.parsePermalinkCanvas(canvas);
+		vesl.ui.parsePermalinkCanvas(canvas);
 	}
 } //}}}
@@ -175,5 +198,5 @@
 function initTexture(gl, imageSource) { //{{{
 	//Initialize textures, or load from memory if they already exist.
-	if (VESL.Helpers.isEmptyOrUndefined(gl.textures[imageSource])) {
+	if (vesl.helpers.isEmptyOrUndefined(gl.textures[imageSource])) {
 		gl.textures[imageSource] = GL.Texture.fromURL(imageSource, {minFilter: gl.LINEAR_MIPMAP_LINEAR, magFilter: gl.LINEAR}, null, gl);
 	}
@@ -210,7 +233,19 @@
 	for (var i = 0; i < properties.length; i++) {
 		object = object[properties[i]];
-		if (VESL.Helpers.isEmptyOrUndefined(object)) { break; }
+		if (vesl.helpers.isEmptyOrUndefined(object)) { break; }
     }
 	return defaultFor(object, value);
+} //}}}
+function triggerStartEvent(canvas) { //{{{
+	// If data markers are enabled for this canvas, wait for their handlers to be registered before triggering onStart event
+	if(!vesl.helpers.isEmptyOrUndefined(canvas.dataMarker) && canvas.dataMarker.enabled) {
+		vesl.dataMarker.areHandlersReady(
+			canvas, 
+			function() {
+				// The onStart event triggers once per plotmodel call load after WebGL and canvas initialization are complete
+				canvas.selector.trigger('onStart', [canvas]);
+			}
+		);
+	}
 } //}}}
 //}}}
@@ -245,10 +280,10 @@
 				//TODO: convert canvas.view.rotation from az/el euler to quaternion
 				if (canvas.view.twod) {
-					canvas.view.position[0] += Math.cos(DEG2RAD * canvas.view.rotation[0]) * deltaX - Math.sin(DEG2RAD * 0) * deltaY;
-					canvas.view.position[2] += Math.sin(DEG2RAD * canvas.view.rotation[0]) * deltaX + Math.cos(DEG2RAD * 0) * deltaY;
+					canvas.view.position[0] += Math.cos(vesl.RATIO_DEG_TO_RAD * canvas.view.rotation[0]) * deltaX - Math.sin(vesl.RATIO_DEG_TO_RAD * 0) * deltaY;
+					canvas.view.position[2] += Math.sin(vesl.RATIO_DEG_TO_RAD * canvas.view.rotation[0]) * deltaX + Math.cos(vesl.RATIO_DEG_TO_RAD * 0) * deltaY;
 				}
 				else {
-					canvas.view.position[0] += Math.cos(DEG2RAD * canvas.view.rotation[0]) * deltaX - Math.sin(DEG2RAD * canvas.view.rotation[0]) * deltaY;
-					canvas.view.position[2] += Math.sin(DEG2RAD * canvas.view.rotation[0]) * deltaX + Math.cos(DEG2RAD * canvas.view.rotation[0]) * deltaY;
+					canvas.view.position[0] += Math.cos(vesl.RATIO_DEG_TO_RAD * canvas.view.rotation[0]) * deltaX - Math.sin(vesl.RATIO_DEG_TO_RAD * canvas.view.rotation[0]) * deltaY;
+					canvas.view.position[2] += Math.sin(vesl.RATIO_DEG_TO_RAD * canvas.view.rotation[0]) * deltaX + Math.cos(vesl.RATIO_DEG_TO_RAD * canvas.view.rotation[0]) * deltaY;
 				}
 			}
@@ -256,6 +291,6 @@
 		//Else, rotate around camera center
 		else {
-			canvas.view.rotation[0] += (canvas.lastDeltaX - ev.deltaX) / canvas.clientWidth * 2 * canvas.controlSensitivity * RAD2DEG;
-			canvas.view.rotation[1] += (canvas.lastDeltaY - ev.deltaY) / canvas.clientHeight * -2 * canvas.controlSensitivity * RAD2DEG;
+			canvas.view.rotation[0] += (canvas.lastDeltaX - ev.deltaX) / canvas.clientWidth * 2 * canvas.controlSensitivity * vesl.RATIO_RAD_TO_DEG;
+			canvas.view.rotation[1] += (canvas.lastDeltaY - ev.deltaY) / canvas.clientHeight * -2 * canvas.controlSensitivity * vesl.RATIO_RAD_TO_DEG;
 			
 			if (canvas.view.rotation[0] > 360) { canvas.view.rotation[0] -= 360; };
@@ -277,19 +312,30 @@
 function onPinch(ev, canvas, displaylog) { //{{{
 	ev.preventDefault();
-	if (ev.type === 'pinchstart') { canvas.view.lastZoom = canvas.view.zoom; }
-	else { 
+	
+	if (ev.type === 'pinchstart') { 
+		canvas.view.lastZoom = canvas.view.zoom; 
+	} else { 
 		canvas.view.zoom = ev.scale * canvas.view.lastZoom;
-		if (displaylog) { console.log(canvas.view.zoom); }
+		
+		if (displaylog) { 
+			console.log(canvas.view.zoom); 
+		}
 	}
 } //}}}
 function onZoom(ev, canvas, displaylog) { //{{{
 	ev.preventDefault();
-	var delta = clamp(ev.scale || ev.wheelDelta || -ev.detail, -1, 1) * canvas.controlSensitivity * canvas.view.zoom / 2;
+	
+	var delta = clamp(ev.scale || ev.wheelDelta || -ev.detail, -1, 1) * canvas.controlSensitivity * canvas.view.zoom / 4;
+	
 	modifyZoom(canvas.view.zoom + delta, canvas, displaylog, ev, 0);
 } //}}}
 function modifyZoom(value, canvas, displaylog, ev, duration) { //{{{
-	if (VESL.Helpers.isEmptyOrUndefined(duration)) duration = 200;
-	var targetZoom = clamp(value, canvas.view.zoomLimits[0], canvas.view.zoomLimits[1]);
+	if (vesl.helpers.isEmptyOrUndefined(duration)) {
+		duration = 200;
+	}
+	
+	var targetZoom 	= clamp(value, canvas.view.zoomLimits[0], canvas.view.zoomLimits[1]);
 	var currentZoom = canvas.view.zoom;
+	
 	animateValue(
 		0,
@@ -299,5 +345,8 @@
 		function(value, info) {
 			canvas.view.zoom = currentZoom * (1 - value) + targetZoom * value;
-			if (displaylog) { console.log(canvas.view.zoom); }
+			
+			if (displaylog) { 
+				console.log(canvas.view.zoom); 
+			}
 			
 			//Trigger any handlers attatched to this canvas event.
@@ -310,17 +359,21 @@
 } //}}}
 function screenToWorldPoint(canvas, x, y) { //{{{
-	var viewportX = (x - canvas.width / 2) / (canvas.width / 2);
-	var viewportY = (canvas.height / 2 - y) / (canvas.height / 2);
-	var origin = vec3.transformMat4(vec3.create(), [viewportX, viewportY, 0], canvas.camera.vpInverseMatrix);
-	return origin;
+	var viewportX 	= (x - canvas.width / 2) / (canvas.width / 2);
+	var viewportY 	= (canvas.height / 2 - y) / (canvas.height / 2);
+	
+	return vec3.transformMat4(vec3.create(), [viewportX, viewportY, 0], canvas.camera.vpInverseMatrix);
 } //}}}
 function screenToModelRay(canvas, x, y, node) { //{{{
-	var inverseMVPMatrix = mat4.invert(mat4.create(), mat4.multiply(mat4.create(), canvas.camera.vpMatrix, node.modelMatrix));
-	var viewportX = (x - canvas.width / 2) / (canvas.width / 2);
-	var viewportY = (canvas.height / 2 - y) / (canvas.height / 2);
-	var origin = vec3.transformMat4(vec3.create(), [viewportX, viewportY, 0], inverseMVPMatrix);
-	var far = vec3.transformMat4(vec3.create(), [viewportX, viewportY, 1.0], inverseMVPMatrix);
-	var direction = vec3.normalize(vec3.create(), vec3.subtract(vec3.create(), far, origin));
-	return {'origin':origin, 'direction':direction};
+	var inverseMVPMatrix 	= mat4.invert(mat4.create(), mat4.multiply(mat4.create(), canvas.camera.vpMatrix, node.modelMatrix));
+	var viewportX 			= (x - canvas.width / 2) / (canvas.width / 2);
+	var viewportY 			= (canvas.height / 2 - y) / (canvas.height / 2);
+	var origin 				= vec3.transformMat4(vec3.create(), [viewportX, viewportY, 0], inverseMVPMatrix);
+	var far 				= vec3.transformMat4(vec3.create(), [viewportX, viewportY, 1.0], inverseMVPMatrix);
+	var direction 			= vec3.normalize(vec3.create(), vec3.subtract(vec3.create(), far, origin));
+	
+	return 	{
+				'origin'	: origin, 
+				'direction'	: direction
+			};
 } //}}}
 function raycast(canvas, origin, direction, node) { //{{{
@@ -328,10 +381,16 @@
 	//Returns hit objects with hit position, normals, barycentric coordinates, element number, and indices of ray-triangle intersection.
 	//TODO: Diagnose marker issues with orthographic views and slr-eustatic updates when switching between basins.
-	if (!node.octree) { node.octree = new GL.Octree(node.mesh); }
+	if (!node.octree) { 
+		node.octree = new GL.Octree(node.mesh); 
+	}
 	
 	var hit = node.octree.testRay(origin, direction, 1e3, 1e10);
-	if (!hit) { return; }
+	
+	if (!hit) { 
+		return; 
+	}
 
 	hit.modelPos = vec3.copy(vec3.create(), hit.pos);
+	
 	vec3.transformMat4(hit.pos, hit.pos, node.modelMatrix);
 	
@@ -343,13 +402,14 @@
 	//TODO: Diagnose marker issues with orthographic views and slr-eustatic updates when switching between basins.
 	var ray = screenToModelRay(canvas, x, y, node);
+	
 	return raycast(canvas, ray.origin, ray.direction, node);
 } //}}}
 function animateValue(current, target, duration, easing, stepCallback, doneCallback) { //{{{
 	//Animates scalar value for length duration, calling callback each step. Specify smooth easing as a string ('swing', 'linear').
-	$({'value':current}).animate({'value':target}, {
-		duration: duration,
-		easing: easing,
-		step: stepCallback,
-		done: doneCallback
+	$({'value':current}).animate({'value' : target}, {
+		duration	: duration,
+		easing		: easing,
+		step		: stepCallback,
+		done		: doneCallback
 	});
 } //}}}
@@ -358,16 +418,32 @@
 function updateCameraMatrix(canvas) { //{{{
     //Update view matrix and multiply with projection matrix to get the view-projection matrix.
-	var vMatrix = mat4.create();
-	var pMatrix = mat4.create();
-	var translateMatrix = mat4.create();
-	var rotationMatrix = mat4.create();
-	var azimuthRotationMatrix = mat4.create();
-	var elevationRotationMatrix = mat4.create();
-	var aspectRatio = canvas.clientWidth / canvas.clientHeight;
-	var camera = canvas.camera;
-	var view = canvas.view;
+	var vMatrix 					= mat4.create();
+	var pMatrix 					= mat4.create();
+	var translateMatrix 			= mat4.create();
+	var rotationMatrix 				= mat4.create();
+	var azimuthRotationMatrix 		= mat4.create();
+	var elevationRotationMatrix 	= mat4.create();
+	var aspectRatio 				= canvas.clientWidth / canvas.clientHeight;
+	var camera 						= canvas.camera;
+	var view 						= canvas.view;
 
-	if (view.twod) { mat4.ortho(pMatrix, -aspectRatio*6.371e6/view.zoom, aspectRatio*6.371e6/view.zoom, -6.371e6/view.zoom, 6.371e6/view.zoom, camera.near, camera.far); }
-	else { mat4.perspective(pMatrix, camera.fov * DEG2RAD, aspectRatio, camera.near, camera.far); }
+	if (view.twod) { 
+		mat4.ortho(
+			pMatrix, -aspectRatio * 6.371e6 / view.zoom, 
+			aspectRatio * 6.371e6 / view.zoom, 
+			-6.371e6 / view.zoom, 
+			6.371e6 / view.zoom, 
+			camera.near, 
+			camera.far
+		); 
+	} else { 
+		mat4.perspective(
+			pMatrix, 
+			camera.fov * vesl.RATIO_DEG_TO_RAD,
+			aspectRatio, 
+			camera.near, 
+			camera.far
+		); 
+	}
 	
 	//Apply worldspace translation
@@ -376,15 +452,14 @@
 	//Calculate rotation around camera focal point about worldspace origin
 	if (view.twod) {
-		mat4.rotate(azimuthRotationMatrix, azimuthRotationMatrix, -DEG2RAD * 0, [0, 1, 0]);
-		mat4.rotate(elevationRotationMatrix, elevationRotationMatrix, DEG2RAD * 90, [1, 0, 0]);
+		mat4.rotate(azimuthRotationMatrix, azimuthRotationMatrix, -vesl.RATIO_DEG_TO_RAD * 0, [0, 1, 0]);
+		mat4.rotate(elevationRotationMatrix, elevationRotationMatrix, vesl.RATIO_DEG_TO_RAD * 90, [1, 0, 0]);
 		mat4.multiply(rotationMatrix, elevationRotationMatrix, azimuthRotationMatrix);
-	}
-	else {
-		mat4.rotate(azimuthRotationMatrix, azimuthRotationMatrix, -DEG2RAD * (view.rotation[0] + 90), [0, 1, 0]);
-		mat4.rotate(elevationRotationMatrix, elevationRotationMatrix, DEG2RAD * view.rotation[1], [1, 0, 0]);
+	} else {
+		mat4.rotate(azimuthRotationMatrix, azimuthRotationMatrix, -vesl.RATIO_DEG_TO_RAD * (view.rotation[0] + 90), [0, 1, 0]);
+		mat4.rotate(elevationRotationMatrix, elevationRotationMatrix, vesl.RATIO_DEG_TO_RAD * view.rotation[1], [1, 0, 0]);
 		mat4.multiply(rotationMatrix, elevationRotationMatrix, azimuthRotationMatrix);
-		//var quaternionWorldX = Node.prototype.eulerToQuaternion(0, 0, DEG2RAD * (view.rotation[0]));
-		//var quaternionWorldY = Node.prototype.eulerToQuaternion(0, DEG2RAD * (view.rotation[1]), 0);
-		//var quaternionWorldZ = Node.prototype.eulerToQuaternion(DEG2RAD * (view.rotation[2]), 0, 0);
+		//var quaternionWorldX = Node.prototype.eulerToQuaternion(0, 0, vesl.RATIO_DEG_TO_RAD * (view.rotation[0]));
+		//var quaternionWorldY = Node.prototype.eulerToQuaternion(0, vesl.RATIO_DEG_TO_RAD * (view.rotation[1]), 0);
+		//var quaternionWorldZ = Node.prototype.eulerToQuaternion(vesl.RATIO_DEG_TO_RAD * (view.rotation[2]), 0, 0);
 		//var quaternionTemp = quat.multiply(quat.create(), quaternionWorldY, quaternionWorldX);
 		//quat.multiply(camera.rotation, quaternionWorldZ, quaternionTemp);
@@ -397,5 +472,5 @@
 	//Apply screenspace translation to emulate rotation around point
 	mat4.identity(translateMatrix);
-	mat4.translate(translateMatrix, translateMatrix, [0.0, 0.0, -6.371e6/view.zoom]);
+	mat4.translate(translateMatrix, translateMatrix, [0.0, 0.0, -6.371e6 / view.zoom]);
 	mat4.multiply(vMatrix, translateMatrix, vMatrix);
 	
@@ -415,21 +490,34 @@
 }//}}}
 function drawSceneGraphNode(canvas, node) { //{{{
-	if (!node.enabled) { return; }
+	if (!node.enabled) { 
+		return;
+	}
 
 	var gl = canvas.gl;
+	
 	gl.makeCurrent();
 	
 	var mvpMatrix = mat4.create();
+	
 	mat4.multiply(mvpMatrix, canvas.camera.vpMatrix, node.modelMatrix);
 	
-	var normalMatrix = mat3.create();
-	var tempMatrix = mat4.create();
+	var normalMatrix 	= mat3.create();
+	var tempMatrix 		= mat4.create();
+	
 	mat4.invert(tempMatrix, node.modelMatrix);
 	mat4.transpose(tempMatrix, tempMatrix);
 	mat3.fromMat4(normalMatrix, tempMatrix);
 	
-	if (node.texture) { node.texture.bind(0); }
-	if (node.disableDepthTest) { gl.disable(gl.DEPTH_TEST); }
-	if (node.enableCullFace) { gl.enable(gl.CULL_FACE); }
+	if (node.texture) { 
+		node.texture.bind(0); 
+	}
+	
+	if (node.disableDepthTest) { 
+		gl.disable(gl.DEPTH_TEST); 
+	}
+	
+	if (node.enableCullFace) { 
+		gl.enable(gl.CULL_FACE); 
+	}
 
 	gl.cullFace(node.cullFace);
@@ -438,5 +526,5 @@
 	
 	//Setup for light that originates from camera
-	var atm = canvas.atmosphere;
+	var atm 		= canvas.atmosphere;
 	var lightOrigin = vec3.create();
 	
@@ -495,28 +583,35 @@
 function canvasResize(canvas) {
 	var rect = canvas.getBoundingClientRect();
+	
 	canvas.width  = rect.width;
 	canvas.height = rect.height;
+	
 	canvas.gl.viewport(0, 0, canvas.width, canvas.height);
 	
-	if (!VESL.Helpers.isEmptyOrUndefined(canvas.overlaycanvas)) {
-		rect = canvas.overlaycanvas.getBoundingClientRect();
-		canvas.overlaycanvas.width  = rect.width;
-		canvas.overlaycanvas.height = rect.height;
+	var overlaycanvas = canvas.overlaycanvas;
+	
+	if (!vesl.helpers.isEmptyOrUndefined(overlaycanvas)) {
+		rect = overlaycanvas.getBoundingClientRect();
+		overlaycanvas.width  = rect.width;
+		overlaycanvas.height = rect.height;
+		overlaycanvas.getContext('2d').clearRect(0, 0, overlaycanvas.width, overlaycanvas.height);
 	}
 }
-function draw(canvas) { //{{{
+function drawCanvas(canvas) { //{{{
 	//Ensure all nodes are ready to render
 	//TODO: Come up with better way to check if shaders are ready, or move outside of main draw function
 	var nodes = canvas.nodes;
+	
 	if (!canvas.draw.ready) {
 		if (Object.keys(nodes).length !== 0) {
 			canvas.draw.ready = true;
+			
 			for (var node in nodes) {
 				if (nodes[node].shader.ready === false) {
 					canvas.draw.ready = false;
+					
 					break;
 				}
 			}
-			
 		}
 	}
@@ -528,4 +623,5 @@
 		
 		var gl = canvas.gl;
+		
 		gl.makeCurrent(); //litegl function to handle switching between multiple canvases
 		gl.clearColor(canvas.backgroundcolor[0], canvas.backgroundcolor[1], canvas.backgroundcolor[2], canvas.backgroundcolor[3]);
@@ -537,10 +633,16 @@
 		canvas.selector.trigger('onPreRender', [canvas]);
 		
-		for (var handler in canvas.overlayHandlers) { canvas.overlayHandlers[handler](canvas); }
+		for (var handler in canvas.overlayHandlers) { 
+			canvas.overlayHandlers[handler](canvas);
+		}
 		
 		var drawPassNumber = 3;
+		
+		// NOTE: For large value of drawPassNumber/nodes.length, could copy nodes to a new array and pop elements as each drawOrder is found
 		for (var i = drawPassNumber - 1; i >= 0; --i) {
 			for (var node in nodes) {
-				if (nodes[node].drawOrder === i) { drawSceneGraphNode(canvas, nodes[node]); }
+				if (nodes[node].drawOrder === i) { 
+					drawSceneGraphNode(canvas, nodes[node]); 
+				}
 			}
 		}
@@ -548,5 +650,5 @@
 	
 	//Regardless of ready state, schedule next frame to check for ready state and render
-	canvas.draw.handler = window.requestAnimationFrame(function(time) { draw(canvas); });
+	canvas.draw.handler = window.requestAnimationFrame(function(time) { drawCanvas(canvas); });
 } //}}}
 //}}}
Index: /issm/trunk-jpl/src/m/plot/webgl_node.js
===================================================================
--- /issm/trunk-jpl/src/m/plot/webgl_node.js	(revision 22893)
+++ /issm/trunk-jpl/src/m/plot/webgl_node.js	(revision 22894)
@@ -106,5 +106,5 @@
 	var yaw = Math.atan2(t3, t4);
 	
-	return [pitch * RAD2DEG, roll * RAD2DEG, yaw * RAD2DEG];
+	return [pitch * vesl.RATIO_RAD_TO_DEG, roll * vesl.RATIO_RAD_TO_DEG, yaw * vesl.RATIO_RAD_TO_DEG];
 } //}}}
 Node.prototype.updateModelMatrix = function() { //{{{
@@ -122,16 +122,16 @@
 	var rotationMatrix = mat4.create();
 	var zRotationMatrix = mat4.create();	
-	mat4.rotate(zRotationMatrix, zRotationMatrix, DEG2RAD * this.rotation[2], [0.0, 0.0, 1.0]);
+	mat4.rotate(zRotationMatrix, zRotationMatrix, vesl.RATIO_DEG_TO_RAD * this.rotation[2], [0.0, 0.0, 1.0]);
 	mat4.multiply(rotationMatrix, zRotationMatrix, rotationMatrix);
 	var yRotationMatrix = mat4.create();	
-	mat4.rotate(yRotationMatrix, yRotationMatrix, DEG2RAD * this.rotation[1], [0.0, 1.0, 0.0]);
+	mat4.rotate(yRotationMatrix, yRotationMatrix, vesl.RATIO_DEG_TO_RAD * this.rotation[1], [0.0, 1.0, 0.0]);
 	mat4.multiply(rotationMatrix, yRotationMatrix, rotationMatrix);
 	var xRotationMatrix = mat4.create();	
-	mat4.rotate(xRotationMatrix, xRotationMatrix, DEG2RAD * this.rotation[0], [1.0, 0.0, 0.0]);
+	mat4.rotate(xRotationMatrix, xRotationMatrix, vesl.RATIO_DEG_TO_RAD * this.rotation[0], [1.0, 0.0, 0.0]);
 	mat4.multiply(rotationMatrix, xRotationMatrix, rotationMatrix);
 	mat4.multiply(modelMatrix, rotationMatrix, modelMatrix);
 
-	//var rotationQuaternionX = this.eulerToQuaternion(0, -DEG2RAD * this.rotation[0], 0);
-	//var rotationQuaternionY = this.eulerToQuaternion(DEG2RAD * this.rotation[1], 0, 0);
+	//var rotationQuaternionX = this.eulerToQuaternion(0, -vesl.RATIO_DEG_TO_RAD * this.rotation[0], 0);
+	//var rotationQuaternionY = this.eulerToQuaternion(vesl.RATIO_DEG_TO_RAD * this.rotation[1], 0, 0);
 	//mat4.fromQuat(this.rotationMatrix, quat.multiply(quat.create(), rotationQuaternionY, rotationQuaternionX));
 
@@ -163,5 +163,5 @@
 } //}}}
 Node.prototype.transform = function() { //{{{
-	//Transforms the translation, rotation, or scle fo the node and updates the model matrix.
+	//Transforms the translation, rotation, or scale of the node and updates the model matrix.
 	var args = Array.prototype.slice.call(arguments);
 	var options = new pairoptions(args.slice(0,args.length));
@@ -171,7 +171,7 @@
 	var scale = options.getfieldvalue('scale', undefined);
 	
-	if (!VESL.Helpers.isEmptyOrUndefined(translation)) this.translation = translation;
-	if (!VESL.Helpers.isEmptyOrUndefined(rotation)) this.rotation = rotation;
-	if (!VESL.Helpers.isEmptyOrUndefined(scale)) this.scale = scale;
+	if (!vesl.helpers.isEmptyOrUndefined(translation)) this.translation = translation;
+	if (!vesl.helpers.isEmptyOrUndefined(rotation)) this.rotation = rotation;
+	if (!vesl.helpers.isEmptyOrUndefined(scale)) this.scale = scale;
 	this.updateModelMatrix();
 } //}}}
@@ -212,5 +212,5 @@
 	var face;
 	
-	if (VESL.Helpers.isEmptyOrUndefined(faceVertexCData)) {
+	if (vesl.helpers.isEmptyOrUndefined(faceVertexCData)) {
 		vertexArray = new Float32Array(vertices[0].length * 3);
 		for(var i = 0, v = 0; i < vertices[0].length; i++) {	
@@ -289,5 +289,5 @@
 	var face;
 	
-	if (VESL.Helpers.isEmptyOrUndefined(faceVertexCData)) { return; }
+	if (vesl.helpers.isEmptyOrUndefined(faceVertexCData)) { return; }
 	
 	//Use logarithmic scaling if it is valid
@@ -368,6 +368,6 @@
 	}
 	
-	if (this.computeIndices === true && !VESL.Helpers.isEmptyOrUndefined(faces)) {
-		if (!VESL.Helpers.isEmptyOrUndefined(faces[0])) { //Check for 2D format and process if needed
+	if (this.computeIndices === true && !vesl.helpers.isEmptyOrUndefined(faces)) {
+		if (!vesl.helpers.isEmptyOrUndefined(faces[0])) { //Check for 2D format and process if needed
 			if (faceColor !== 'none') { //Check for triangle rendering
 				indexArray = new Uint16Array(faces.length * 3);
@@ -405,5 +405,5 @@
 	
 	var coords = options.getfieldvalue('Coords', undefined);
-	if (!VESL.Helpers.isEmptyOrUndefined(coords)) {
+	if (!vesl.helpers.isEmptyOrUndefined(coords)) {
 		this.patchCoords(coords, this.faces, this.vertices);
 		var buffer = this.mesh.getBuffer("coords");
@@ -415,5 +415,5 @@
 	//Computes and caches octrees for a node.
 	var octree = this.canvas.octrees[this.name];
-	if (VESL.Helpers.isEmptyOrUndefined(octree)) {
+	if (vesl.helpers.isEmptyOrUndefined(octree)) {
 		octree = new GL.Octree(this.mesh);
 	}
@@ -441,5 +441,5 @@
 			var coordinateObject = vertices[i];
 			var j = 0;
-			if (VESL.Helpers.isEmptyOrUndefined(indices)) {
+			if (vesl.helpers.isEmptyOrUndefined(indices)) {
 				for (var key in coordinateObject) {
 					console.log(key);
@@ -486,19 +486,21 @@
 	//Scales and returns vertices x, y, and z by factor scale. Uses md.geometry.scale for heightscaling in 3d meshes.
 	var region = maskObject.region;
-	var mask = maskObject.enabled ? maskObject.heightmask[region] : [];
+	var mask;
 	var maskScale = maskObject.enabled ? maskObject.scale : 1;
 	
-	//The maskScaleField is the height scaling array. Use one if provided by maskObject.field, otherwise use md.geometry.surface
-	var maskScaleField = !VESL.Helpers.isEmptyOrUndefined(maskObject.scaleField) ? maskObject.scaleField : md.geometry.surface;
-	//If md.geometry.surface was empty and was used as maskScaleField, assign an empty array to both.
-	if (!maskScaleField) {
-		md.geometry.surface = NewArrayFill(md.mesh.x.length,0);
+	//The maskScaleField is the height scaling array.
+	var maskScaleField = maskObject.scaleField;
+	if (Array.isArray(md.geometry.surface) && md.geometry.surface[0] === undefined) {
+		md.geometry.surface = NewArrayFill(x.length, 1);
+	}
+	if (vesl.helpers.isNaN(maskScaleField) || maskScaleField === undefined || (Array.isArray(maskScaleField) && maskScaleField[0]  === undefined)) {
 		maskScaleField = md.geometry.surface;
 	}
-	
+		
 	//Scale in 3D if using globe model, or in 2D otherwise.
 	if (md.mesh.classname() === 'mesh3dsurface') {
 		var element;
 		if (x.length === md.mesh.numberofelements) { //element plot
+			mask = maskObject.enabled ? maskObject.heightmask[region] : NewArrayFill(elements.length, 1);
 			for (var i = 0; i < x.length; i++) {
 				if (maskObject.enabled) { //Scale the element if mask is not enabled, or if it is, only if it is also in the mask
@@ -517,4 +519,5 @@
 		}
 		else if (x.length === md.mesh.numberofvertices) { //node plot
+			mask = maskObject.enabled ? maskObject.heightmask[region] : NewArrayFill(x.length, 1);
 			for (var i = 0; i < x.length; i++) {
 				if (!maskObject.enabled || mask[i] === 1) { //Scale the node if mask is not enabled, or if it is, only if it is also in the mask
@@ -530,4 +533,5 @@
 	} else {
 		z = z.slice();
+		mask = maskObject.enabled ? maskObject.heightmask[region] : NewArrayFill(z.length, 1);
 		var zMin = ArrayMin(maskScaleField);
 		for(var i = 0; i < z.length; i++) {
