Index: /issm/trunk-jpl/src/m/classes/clusters/generic.js
===================================================================
--- /issm/trunk-jpl/src/m/classes/clusters/generic.js	(revision 21682)
+++ /issm/trunk-jpl/src/m/classes/clusters/generic.js	(revision 21683)
@@ -47,5 +47,9 @@
 	} //}}}
 	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();
 		$(callbackid).html(sprintf("%-16s", "CONNECTING...")).prop("disabled", true);
Index: sm/trunk-jpl/src/m/io/download.js
===================================================================
--- /issm/trunk-jpl/src/m/io/download.js	(revision 21682)
+++ 	(revision )
@@ -1,31 +1,0 @@
-function download() {
-	//DOWNLOAD - save model/variable data to file
-	//
-	//   Usage:
-	//      download=download('id','slr-download');
-	//      download=download('id','slr-download','data',md.geometry.thickness,'data',md.results.Stressbalance.Vel);
-
-	//Convert arguments to options
-	var args = Array.prototype.slice.call(arguments);
-	var options = new pairoptions(args.slice());
-	
-	//Recover option values:
-	var id = options.getfieldvalue('id','');
-	
-	document.getElementById(id).addEventListener('click', function() {
-		var data = {'elements':md.mesh.elements,'x':md.mesh.x,'y':md.mesh.y,'z':md.mesh.z};
-		for (var i=2; i < args.length; i+=2) {
-			try { 
-				data[args[i]] = args[i+1];
-			}
-			catch (e) { 
-				console.log(e); 
-			}
-		}
-		string = JSONfn.stringify(data);
-		
-		var url='data:text/json:charset=utf8,' + encodeURIComponent(string);
-		window.open(url, '_blank');
-		window.focus();
-	});
-}
Index: sm/trunk-jpl/src/m/io/saveAsFile.js
===================================================================
--- /issm/trunk-jpl/src/m/io/saveAsFile.js	(revision 21682)
+++ 	(revision )
@@ -1,6 +1,0 @@
-function saveAsFile(content) {
-
-	var url='data:text/json:charset=utf8,' + encodeURIComponent(content);
-	window.open(url, '_blank');
-	window.focus();
-}
Index: /issm/trunk-jpl/src/m/plot/applyoptions.js
===================================================================
--- /issm/trunk-jpl/src/m/plot/applyoptions.js	(revision 21682)
+++ /issm/trunk-jpl/src/m/plot/applyoptions.js	(revision 21683)
@@ -1,31 +1,31 @@
-function applyoptions(md,data,datatype,options,canvas,gl,node){ //{{{
+function applyoptions(md, data, datatype, options, canvas, gl, node){ //{{{
 	//APPLYOPTIONS - apply colobar, text, cloud, and expdisp options to current plot
 	//
 	//   Usage:
-	//      applyoptions(md,data,options)
+	//      applyoptions(md, data, options)
 	//
 	//   See also: PLOTMODEL, PARSE_OPTIONS
 	
-	//colorbar {{{
+	//{{{ colorbar
 	if (options.exist('colorbar')) {
 		if (options.getfieldvalue('colorbar')==1) {
-			//Handle movie data {{{
+			//{{{ Handle movie data
 			if (datatype == 5) {
 				data = data[0];
 			} //}}}
-			//Variable options initialization {{{
-			var caxis = options.getfieldvalue('caxis',[ArrayMin(data),ArrayMax(data)]);
+			//{{{ Variable options initialization
+			var caxis = options.getfieldvalue('caxis',[ArrayMin(data), ArrayMax(data)]);
 			var colorbarinnerlabels = options.getfieldvalue('colorbarinnerlabels','off');
-			var ccanvasid,ctitleid,clabelsid,ccanvas,ctitle,clabels,ccontext,cmap,colorbar,cwidth,cheight,cgradient,color,y,x;
-			//}}}
-			//Create colorbar labels {{{
+			var ccanvasid, ctitleid, clabelsid, ccanvas, ctitle, clabels, ccontext, cmap, colorbar, cwidth, cheight, cgradient, color, y, x;
+			//}}}
+			//{{{ Create colorbar labels 
 			var labels = [];
-			var cdivisions = options.getfieldvalue('colorbarnticks',6);
+			var cdivisions = options.getfieldvalue('colorbarnticks', 6);
 			var caxisdelta = caxis[1] - caxis[0];
-			var precision = options.getfieldvalue('colorbarprecision',3);
+			var precision = options.getfieldvalue('colorbarprecision', 3);
 			if (options.getfieldvalue('log','off')!='off') {
 				for (var i=cdivisions; i >= 0; i--) {
-					var scale = (Math.log10(caxis[1])-Math.log10(caxis[0]))/Math.log10(options.getfieldvalue('log',10));
-					labels[i] = (Math.pow(options.getfieldvalue('log',10),Math.log10(caxis[0])/Math.log10(options.getfieldvalue('log',10))+scale*(cdivisions-i)/cdivisions)).toFixed(precision);
+					var scale = (Math.log10(caxis[1])-Math.log10(caxis[0]))/Math.log10(options.getfieldvalue('log', 10));
+					labels[i] = (Math.pow(options.getfieldvalue('log', 10), Math.log10(caxis[0])/Math.log10(options.getfieldvalue('log', 10))+scale*(cdivisions-i)/cdivisions)).toFixed(precision);
 				}
 			} else {
@@ -34,33 +34,33 @@
 				}
 			} //}}}
-			//Initialize colorbar canvas {{{
-			ccanvasid = options.getfieldvalue('colorbarid',options.getfieldvalue('canvasid').replace('canvas','colorbar-canvas'));			
+			//{{{ Initialize colorbar canvas
+			ccanvasid = options.getfieldvalue('colorbarid', options.getfieldvalue('canvasid').replace('canvas','colorbar-canvas'));			
 			ccanvas = $('#'+ccanvasid)[0];
-			cwidth = ccanvas.width*options.getfieldvalue('colorbarwidth',1);
-			cheight = ccanvas.height*options.getfieldvalue('colorbarheight',1);
+			cwidth = ccanvas.width*options.getfieldvalue('colorbarwidth', 1);
+			cheight = ccanvas.height*options.getfieldvalue('colorbarheight', 1);
 			ccontext = ccanvas.getContext('2d');
-			ccontext.clearRect(0,0, cwidth, cheight);
+			ccontext.clearRect(0, 0, cwidth, cheight);
 			ccontext.beginPath();
 			cmap = options.getfieldvalue('colormap','jet');
 			colorbar = colorbars[cmap];
-			cgradient = ccontext.createLinearGradient(0,0,0,cheight);
-			//}}}
-			//Draw colorbar gradient {{{
+			cgradient = ccontext.createLinearGradient(0, 0, 0, cheight);
+			//}}}
+			//{{{ Draw colorbar gradient
 			for (var i=0; i < colorbar.length; i++) {
 				color = colorbar[colorbar.length-i-1];
-				color = [Math.round(color[0]*255),Math.round(color[1]*255),Math.round(color[2]*255)];	
-				cgradient.addColorStop(i/colorbar.length*(cdivisions/(cdivisions+1.0))+(1.0/(cdivisions+1.0)),'rgba('+color.toString()+',1.0)');
+				color = [Math.round(color[0]*255), Math.round(color[1]*255), Math.round(color[2]*255)];	
+				cgradient.addColorStop(i/colorbar.length*(cdivisions/(cdivisions+1.0))+(1.0/(cdivisions+1.0)),'rgba('+color.toString()+', 1.0)');
 			}
 			ccontext.fillStyle=cgradient;
-			ccontext.fillRect(0,0,cwidth,cheight);
-			//}}}
-			//Draw colorbar border {{{
+			ccontext.fillRect(0, 0, cwidth, cheight);
+			//}}}
+			//{{{ Draw colorbar border
 			ccontext.beginPath();
 			ccontext.lineWidth='1';
 			ccontext.strokeStyle=options.getfieldvalue('colorbarfontcolor','black');
-			ccontext.rect(0,0,cwidth,cheight);
+			ccontext.rect(0, 0, cwidth, cheight);
 			ccontext.stroke();
 			//}}}
-			//Draw colorbar labels {{{
+			//{{{ Draw colorbar labels
 			clabelsid = options.getfieldvalue('colorbarid', ccanvasid).replace('canvas','labels');
 			clabels = $('#'+clabelsid);
@@ -80,13 +80,13 @@
 				clabelstring += '<li><span>'+labels[i]+'</span></li>';
 				ccontext.beginPath();
-				ccontext.moveTo(0,y);
-				ccontext.lineTo(x,y);
-				ccontext.moveTo(cwidth-x,y);
-				ccontext.lineTo(cwidth,y);
+				ccontext.moveTo(0, y);
+				ccontext.lineTo(x, y);
+				ccontext.moveTo(cwidth-x, y);
+				ccontext.lineTo(cwidth, y);
 				ccontext.stroke();
 			}
 			clabels.append(clabelstring);
 			//}}}
-			//Draw colorbar title {{{
+			//{{{ Draw colorbar title
 			ctitleid = options.getfieldvalue('colorbarid', ccanvasid).replace('canvas','heading');
 			ctitle = $('#'+ctitleid);
@@ -95,6 +95,6 @@
 		} 
 	} //}}}
-	//texture canvas //{{{
-	var tcontext,tcanvas,tcanvasid,tURL,tgradient;
+	//{{{ texture canvas
+	var tcontext, tcanvas, tcanvasid, tURL, tgradient;
 	tcanvasid = 'texturecanvas';
 	var tcanvas = document.getElementById(tcanvasid);
@@ -104,5 +104,5 @@
 	}
 	tcontext = tcanvas.getContext('2d');
-	tgradient = tcontext.createLinearGradient(0,0,0,256);
+	tgradient = tcontext.createLinearGradient(0, 0, 0, 256);
 		
 	var cmap = options.getfieldvalue('colormap','jet');
@@ -110,18 +110,18 @@
 	for (var i=0; i < colorbar.length; i++) {
 		color = colorbar[colorbar.length-i-1];
-		color = [Math.round(color[0]*255),Math.round(color[1]*255),Math.round(color[2]*255)];	
-		tgradient.addColorStop(i/colorbar.length,'rgba('+color.toString()+',1.0)');
+		color = [Math.round(color[0]*255), Math.round(color[1]*255), Math.round(color[2]*255)];	
+		tgradient.addColorStop(i/colorbar.length,'rgba('+color.toString()+', 1.0)');
 	}
 	
 	tcontext.fillStyle = tgradient;
-	tcontext.fillRect(0,0,256,256);
+	tcontext.fillRect(0, 0, 256, 256);
 	tURL = tcanvas.toDataURL();
-	node.texture = initTexture(gl,tURL);
+	node.texture = initTexture(gl, tURL);
 	node.textureCanvas = tcanvas;
-	node.caxis = options.getfieldvalue('caxis',[ArrayMin(data),ArrayMax(data)]);
+	node.caxis = options.getfieldvalue('caxis',[ArrayMin(data), ArrayMax(data)]);
 	//}}}
-	//expdisp contours {{{
+	//{{{ expdisp contours
 	if (options.exist('expdisp')) {
-		canvas.nodes.expdisp = Node(gl,options);
+		canvas.nodes.expdisp = Node(gl, options);
 		var node = canvas.nodes.expdisp;
 		
@@ -131,7 +131,7 @@
 		var colors = [];
 		var rgbcolor = [];
-		var xmin,xmax;
-		var ymin,ymax;
-		var zmin,zmax;
+		var xmin, xmax;
+		var ymin, ymax;
+		var zmin, zmax;
 		var scale;
 		
@@ -139,5 +139,5 @@
 		var x = options.getfieldvalue('expdisp').x;
 		var y = options.getfieldvalue('expdisp').y;
-		var z = Array.apply(null, Array(x.length)).map(Number.prototype.valueOf,0);
+		var z = Array.apply(null, Array(x.length)).map(Number.prototype.valueOf, 0);
 		
 		if (options.getfieldvalue('expdisp').z) {
@@ -147,10 +147,10 @@
 
 		//Compute coordinates and data range: //{{{
-		var modelxlim = [ArrayMin(x),ArrayMax(x)];
-		var modelylim = [ArrayMin(y),ArrayMax(y)];
-		var modelzlim = [ArrayMin(z),ArrayMax(z)];
-		var xlim = options.getfieldvalue('xlim',modelxlim);
-		var ylim = options.getfieldvalue('ylim',modelylim);
-		var zlim = options.getfieldvalue('zlim',modelzlim);
+		var modelxlim = [ArrayMin(x), ArrayMax(x)];
+		var modelylim = [ArrayMin(y), ArrayMax(y)];
+		var modelzlim = [ArrayMin(z), ArrayMax(z)];
+		var xlim = options.getfieldvalue('xlim', modelxlim);
+		var ylim = options.getfieldvalue('ylim', modelylim);
+		var zlim = options.getfieldvalue('zlim', modelzlim);
 		xmin = xlim[0];
 		xmax = xlim[1];
@@ -165,5 +165,5 @@
 		node.shaderName = 'colored';
 		node.shader = gl.shaders[node.shaderName].program;
-		node.scale = [scale, scale, scale*options.getfieldvalue('heightscale',1)];
+		node.scale = [scale, scale, scale*options.getfieldvalue('heightscale', 1)];
 		node.translation = [(xmin + xmax) / (-2 / scale), (ymin + ymax) / (-2 / scale), (zmin + zmax) / (-2 / scale)];
 		node.modelMatrix = updateModelMatrix(node);
@@ -178,5 +178,5 @@
 
 		//retrieve some options
-		var linewidth=options.getfieldvalue('linewidth',1);
+		var linewidth=options.getfieldvalue('linewidth', 1);
 		var edgecolor=options.getfieldvalue('edgecolor','black'); //RGBCOLOR?
 
@@ -199,7 +199,7 @@
 		node.buffers = initBuffers(gl, node.arrays);
 	} //}}}
-	//cloud of points {{{
+	//{{{ cloud of points
 	if (options.exist('cloud')) {
-		canvas.nodes.cloud = Node(gl,options);
+		canvas.nodes.cloud = Node(gl, options);
 		var node = canvas.nodes.cloud;
 
@@ -209,7 +209,7 @@
 		var colors = [];
 		var rgbcolor = [];
-		var xmin,xmax;
-		var ymin,ymax;
-		var zmin,zmax;
+		var xmin, xmax;
+		var ymin, ymax;
+		var zmin, zmax;
 		var scale;
 		
@@ -217,5 +217,5 @@
 		var x = options.getfieldvalue('cloud').x;
 		var y = options.getfieldvalue('cloud').y;
-		var z = Array.apply(null, Array(x.length)).map(Number.prototype.valueOf,0);
+		var z = Array.apply(null, Array(x.length)).map(Number.prototype.valueOf, 0);
 		
 		if (options.getfieldvalue('cloud').z) {
@@ -225,10 +225,10 @@
 
 		//Compute coordinates and data range: //{{{
-		var modelxlim = [ArrayMin(x),ArrayMax(x)];
-		var modelylim = [ArrayMin(y),ArrayMax(y)];
-		var modelzlim = [ArrayMin(z),ArrayMax(z)];
-		var xlim = options.getfieldvalue('xlim',modelxlim);
-		var ylim = options.getfieldvalue('ylim',modelylim);
-		var zlim = options.getfieldvalue('zlim',modelzlim);
+		var modelxlim = [ArrayMin(x), ArrayMax(x)];
+		var modelylim = [ArrayMin(y), ArrayMax(y)];
+		var modelzlim = [ArrayMin(z), ArrayMax(z)];
+		var xlim = options.getfieldvalue('xlim', modelxlim);
+		var ylim = options.getfieldvalue('ylim', modelylim);
+		var zlim = options.getfieldvalue('zlim', modelzlim);
 		xmin = xlim[0];
 		xmax = xlim[1];
@@ -243,5 +243,5 @@
 		node.shaderName = 'colored';
 		node.shader = gl.shaders[node.shaderName].program;
-		node.scale = [scale, scale, scale*options.getfieldvalue('heightscale',1)];
+		node.scale = [scale, scale, scale*options.getfieldvalue('heightscale', 1)];
 		node.translation = [(xmin + xmax) / (-2 / scale), (ymin + ymax) / (-2 / scale), (zmin + zmax) / (-2 / scale)];
 		node.modelMatrix = updateModelMatrix(node);
@@ -256,5 +256,5 @@
 
 		//retrieve some options
-		var linewidth=options.getfieldvalue('linewidth',1);
+		var linewidth=options.getfieldvalue('linewidth', 1);
 		var edgecolor=options.getfieldvalue('edgecolor','black'); //RGBCOLOR?
 
@@ -277,9 +277,8 @@
 		node.buffers = initBuffers(gl, node.arrays);
 	} //}}}
-	
-	//text display //{{{
+	//{{{ text display
 	if (options.exist('textlabels')) {
-		var textcanvas,textcanvasid;	
-		textcanvasid = options.getfieldvalue('textcanvasid',options.getfieldvalue('canvasid')+'-text');
+		var textcanvas, textcanvasid;	
+		textcanvasid = options.getfieldvalue('textcanvasid', options.getfieldvalue('canvasid')+'-text');
 		textcanvas = $('#'+textcanvasid);
 		textcanvas.textlabels = options.getfieldvalue('textlabels',[]);
@@ -287,5 +286,5 @@
 		//setup drawing function for text canvas draw calls
 		textcanvas.draw = function(canvas) {
-			var textcontext,textlabels,textlabel,textcanvaswidth,textcanvasheight,textcoordinates;	
+			var textcontext, textlabels, textlabel, textcanvaswidth, textcanvasheight, textcoordinates;	
 			var textposition = vec3.create();
 			var mvpMatrix = mat4.create();
@@ -304,5 +303,5 @@
 			for (text in textlabels) {
 				textlabel = textlabels[text];
-				mat4.multiply(mvpMatrix, canvas.cameraMatrix, canvas.nodes.overlay.modelMatrix);
+				mat4.multiply(mvpMatrix, canvas.camera.vpMatrix, canvas.nodes.overlay.modelMatrix);
 				textposition = vec3.transformMat4(textposition, textlabel.pos, mvpMatrix);
 				if (textposition[2] > 1) { //clip coordinates with z > 1
@@ -310,5 +309,5 @@
 				}
 				textcoordinates = [(textposition[0]+1.0)/2.0*textcanvaswidth, (-textposition[1]+1.0)/2.0*textcanvasheight]; //NDC to screenspace
-				textcontext.font = String(options.getfieldvalue('colorbarfontsize',18))+'px "Lato",Helvetica,Arial,sans-serif';
+				textcontext.font = String(options.getfieldvalue('colorbarfontsize', 18))+'px "Lato", Helvetica, Arial, sans-serif';
 				textcontext.fillStyle = options.getfieldvalue('colorbarfontcolor','black');
 				textcontext.strokeStyle = options.getfieldvalue('colorbarfontcolor','black');
@@ -321,7 +320,78 @@
 		canvas.textcanvas = textcanvas;
 	} //}}}
-	//Atmosphere {{{
+	//{{{ lat long overlay
+	if (options.exist('latlongoverlay')) {
+		var overlaycanvasid = options.getfieldvalue('latlongoverlayid', options.getfieldvalue('canvasid')+'-overlay');
+		var overlaycanvas = $('#'+overlaycanvasid)[0];
+		var latitudes = {
+			//"-90": 1,
+			//"-65": .999,
+			"-60": 0.994046875,
+			//"-55": 0.983187500000002,
+			//"-50": 0.97173550854167,
+			"-45": 0.955729166666666,
+			//"-40": 0.94218750000000218,
+			//"-35": 0.94218750000000218,
+			"-30": 0.9226562500000024,
+			//"-25": 0.87934895833333526,
+			//"-20": 0.856572916666669,
+			//"-15": 0.830729166666665,
+			//"-10": 0.803552708333336,
+			//"-5": 0.77395833333333541,
+			"0": 0.74218749999999811,
+			//"5": 0.70950364583333347,
+			//"10": 0.67479166666666823,
+			//"15": 0.63932291666666663,
+			//"20": 0.60171875,
+			//"25": 0.563453125,
+			"30": 0.523390625000001,
+			//"35": 0.48401875,
+			//"40": 0.44296875,
+			"45": 0.4020001,
+			//"50": 0.3578125,
+			//"55": 0.311875,
+			"60": 0.26953124999999978,
+			//"65": 0.225390625,
+			//"70": 0.18125,
+			//"75": 0.13541666666666671,
+			//"80": 0.08953125,
+			//"85": 0.046250000000000013,
+			//"90": 0.0,
+		}
+		var longitudes = [-150, -120, -90, -60, -30, 0, 30, 60, 90, 120, 150, 180];
+		overlaycanvas.draw = function(canvas) {
+			var rect = overlaycanvas.getBoundingClientRect();
+			overlaycanvas.width  = rect.width;
+			overlaycanvas.height = rect.height;
+			var ctx = overlaycanvas.getContext('2d');
+			var centerx = overlaycanvas.width / 2;
+			var centery = overlaycanvas.height / 2;
+			var radius = (overlaycanvas.height) / 2;
+			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 = 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]);
+			}
+			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();
+			}
+		}
+		canvas.overlaycanvas = overlaycanvas;
+	} //}}}
+	//{{{ additional rendering nodes
 	if (options.exist('render')) {
-		var meshresults = processmesh(md,data,options);
+		var meshresults = processmesh(md, data, options);
 		var x = meshresults[0]; 
 		var y = meshresults[1]; 
@@ -331,10 +401,10 @@
 		var isplanet = meshresults[5];
 		
-		var modelxlim = [ArrayMin(x),ArrayMax(x)];
-		var modelylim = [ArrayMin(y),ArrayMax(y)];
-		var modelzlim = [ArrayMin(z),ArrayMax(z)];
-		var xlim = options.getfieldvalue('xlim',modelxlim);
-		var ylim = options.getfieldvalue('ylim',modelylim);
-		var zlim = options.getfieldvalue('zlim',modelzlim);
+		var modelxlim = [ArrayMin(x), ArrayMax(x)];
+		var modelylim = [ArrayMin(y), ArrayMax(y)];
+		var modelzlim = [ArrayMin(z), ArrayMax(z)];
+		var xlim = options.getfieldvalue('xlim', modelxlim);
+		var ylim = options.getfieldvalue('ylim', modelylim);
+		var zlim = options.getfieldvalue('zlim', modelzlim);
 		xmin = xlim[0];
 		xmax = xlim[1];
@@ -351,5 +421,4 @@
 			//atmosphere
 			var node = Node(gl);
-			canvas.nodes[canvas.nodes.length] = node;
 			node.name = "atmosphere";
 			node.shaderName = "SkyFromSpace";
@@ -358,15 +427,14 @@
 			node.cullFace = gl.FRONT;
 			node.enableCullFace = true;
-			node.mesh = GL.Mesh.icosahedron({size:6371000*atmosphereScale,subdivisions:6});
-			node.useIndexBuffer = false;
+			node.mesh = GL.Mesh.icosahedron({size: 6371000*atmosphereScale, subdivisions: 6});
 			node.rotation = [0, 0, 0];
 			node.translation = translation;
 			node.center = [0, 0, 0];
 			updateModelMatrix(node);
+			canvas.nodes[node.name] = node;
 		}
 		if (options.getfieldvalue('render',[]).indexOf('space')!=-1) {	
 			//skysphere
 			node = Node(gl);
-			canvas.nodes[canvas.nodes.length] = node;
 			node.name = "skysphere";
 			node.shaderName = "Textured";
@@ -375,11 +443,52 @@
 			node.cullFace = gl.FRONT;
 			node.enableCullFace = true;
-			node.mesh = GL.Mesh.sphere({size:6371000*20});
-			node.texture = initTexture(gl,canvas.rootPath+'textures/TychoSkymapII_t4_2k.jpg');
-			node.useIndexBuffer = false;
+			node.mesh = GL.Mesh.sphere({size: 6371000*20});
+			node.texture = initTexture(gl, canvas.rootPath+'textures/TychoSkymapII_t4_2k.jpg');
 			node.rotation = [0, 0, 0];
 			node.translation = translation;
 			node.center = [0, 0, 0];
 			updateModelMatrix(node);
+			canvas.nodes[node.name] = node;
+		}
+		if (canvas.clouds.enabled) {
+			//clouds
+			for (var i = 0; i < canvas.clouds.quantity; i++) {
+				node = Node(gl);
+				node.name = "clouds" + i;
+				node.shaderName = "Clouds";
+				node.shader = gl.shaders[node.shaderName];
+				node.drawOrder = 2;
+				node.cullFace = gl.BACK;
+				node.enableCullFace = true;
+				node.mesh = GL.Mesh.fromURL(canvas.rootPath+'obj/cloud.obj');
+				node.rotation = [0, 0, 0];
+				node.scale = [2500, 2500, 2500];
+				node.translation = [translation[0], translation[1] - 405000, translation[2]];
+				node.center = [0, 0, 0];
+				node.animation = {"time": Date.now(),"target": node.translation,"current": node.translation};
+				updateModelMatrix(node);
+				canvas.nodes[node.name] = node;
+				//canvas.clouds.list
+			}
+			//TODO: Steven, please add <canvas.clouds.quantity> total cloud nodes, randomly spread over the mesh, giving each one a new name and adding them to the canvas.clouds.list so that we can track them later.
+			
+		}
+		if (options.getfieldvalue('render',[]).indexOf('latlong')!=-1) {	
+			//latlong
+			node = Node(gl);
+			node.name = "clouds";
+			node.shaderName = "Clouds";
+			node.shader = gl.shaders[node.shaderName];
+			node.drawOrder = 2;
+			node.cullFace = gl.BACK;
+			node.enableCullFace = true;
+			node.mesh = GL.Mesh.fromURL(canvas.rootPath+'obj/cloud.obj');
+			node.rotation = [0, 0, 0];
+			node.scale = [2500, 2500, 2500];
+			node.translation = [translation[0], translation[1] - 405000, translation[2]];
+			node.center = [0, 0, 0];
+			node.animation = {"time": Date.now(),"target": node.translation,"current": node.translation};
+			updateModelMatrix(node);
+			canvas.nodes[node.name] = node;
 		}
 	} //}}}
Index: /issm/trunk-jpl/src/m/plot/plot_mesh.js
===================================================================
--- /issm/trunk-jpl/src/m/plot/plot_mesh.js	(revision 21682)
+++ /issm/trunk-jpl/src/m/plot/plot_mesh.js	(revision 21683)
@@ -1,22 +1,22 @@
-function plot_mesh(md,options,canvas) { //{{{
+function plot_mesh(md, options, canvas) { //{{{
 	//PLOT_MESH - Function for plotting wireframe mesh.
 	//
 	//   Usage:
-	//      plot_mesh(md,options,canvas);
+	//      plot_mesh(md, options, canvas);
 	//
 	//   See also: PLOTMODEL, PLOT_MANAGER
 
-	//declare variables:  {{{
+	//{{{ declare variables:
 	var vertices = [];
 	var indices = [];
 	var colors = [];
 	var nanindices = {};
-	var xmin,xmax;
-	var ymin,ymax;
-	var zmin,zmax;
-	var scale,matrixscale,vertexscale;
+	var xmin, xmax;
+	var ymin, ymax;
+	var zmin, zmax;
+	var scale, matrixscale, vertexscale;
 	
 	//Process data and model
-	var meshresults = processmesh(md,[],options);
+	var meshresults = processmesh(md,[], options);
 	var x = meshresults[0]; 
 	var y = meshresults[1]; 
@@ -28,9 +28,9 @@
 	//Compue scaling through matrices for 2d meshes and vertices for 3d meshes
 	if (!md.geometry.surface) {
-		md.geometry.surface=NewArrayFill(md.mesh.x.length,0);
+		md.geometry.surface=NewArrayFill(md.mesh.x.length, 0);
 	}
 	if (md.mesh.classname() == 'mesh3dsurface') {
 		matrixscale = 1;
-		vertexscale = options.getfieldvalue('heightscale',1);
+		vertexscale = options.getfieldvalue('heightscale', 1);
 	}
 	else {
@@ -38,5 +38,5 @@
 			z=md.geometry.surface;
 		}	
-		matrixscale = options.getfieldvalue('heightscale',1);
+		matrixscale = options.getfieldvalue('heightscale', 1);
 		vertexscale = 0;
 	}
@@ -44,10 +44,10 @@
 
 	//Compute coordinates and data range:
-	var modelxlim = [ArrayMin(x),ArrayMax(x)];
-	var modelylim = [ArrayMin(y),ArrayMax(y)];
-	var modelzlim = [ArrayMin(z),ArrayMax(z)];
-	var xlim = options.getfieldvalue('xlim',modelxlim);
-	var ylim = options.getfieldvalue('ylim',modelylim);
-	var zlim = options.getfieldvalue('zlim',modelzlim);
+	var modelxlim = [ArrayMin(x), ArrayMax(x)];
+	var modelylim = [ArrayMin(y), ArrayMax(y)];
+	var modelzlim = [ArrayMin(z), ArrayMax(z)];
+	var xlim = options.getfieldvalue('xlim', modelxlim);
+	var ylim = options.getfieldvalue('ylim', modelylim);
+	var zlim = options.getfieldvalue('zlim', modelzlim);
 	xmin = xlim[0];
 	xmax = xlim[1];
@@ -65,5 +65,5 @@
 	node.shaderName = "Colored";
 	node.shader = gl.shaders[node.shaderName];
-	node.lineWidth = options.getfieldvalue('linewidth',1);
+	node.lineWidth = options.getfieldvalue('linewidth', 1);
 	node.scale = [1, 1, matrixscale];
 	node.rotation = [-90, 0, 0];
@@ -73,6 +73,6 @@
 	node.drawOrder = 0;
 	node.maskEnabled = options.getfieldvalue('innermask','off') == 'on';
-	node.maskHeight = options.getfieldvalue('innermaskheight',150.0)*options.getfieldvalue('heightscale',1);
-	node.maskColor = options.getfieldvalue('innermaskcolor',[0.0,0.0,1.0,1.0]);
+	node.maskHeight = options.getfieldvalue('innermaskheight', 150.0)*options.getfieldvalue('heightscale', 1);
+	node.maskColor = options.getfieldvalue('innermaskcolor',[0.0, 0.0, 1.0, 1.0]);
 	updateModelMatrix(node);
 
@@ -80,7 +80,7 @@
 	var edgecolor = new RGBColor(options.getfieldvalue('edgecolor','black'));
 	if (edgecolor.ok) edgecolor = [edgecolor.r/255.0, edgecolor.g/255.0, edgecolor.b/255.0, 1.0];
-	else throw Error(sprintf("s%s%s\n","initWebGL error message: cound not find out edgecolor color for curent canvas ",canvas));
+	else throw Error(sprintf("s%s%s\n","initWebGL error message: cound not find out edgecolor color for curent canvas ", canvas));
 
-	//node plot {{{
+	//{{{ node plot
 	if (elements[0].length==6){ //prisms
 	}
@@ -136,4 +136,4 @@
 	}
 	//}}}
-	node.mesh = GL.Mesh.load({vertices:vertices, colors:colors, triangles:indices}, null, null, gl);
+	node.mesh = GL.Mesh.load({vertices: vertices, colors: colors, triangles: indices}, null, null, gl);
 } //}}}
Index: /issm/trunk-jpl/src/m/plot/plot_overlay.js
===================================================================
--- /issm/trunk-jpl/src/m/plot/plot_overlay.js	(revision 21682)
+++ /issm/trunk-jpl/src/m/plot/plot_overlay.js	(revision 21683)
@@ -1,22 +1,22 @@
-function plot_overlay(md,data,options,canvas){ //{{{
+function plot_overlay(md, data, options, canvas){ //{{{
 	//PLOT_OVERLAY - Function for plotting a georeferenced image.  
 	//
 	//   Usage:
-	//      plot_overlay(md,data,options,canvas);
+	//      plot_overlay(md, data, options, canvas);
 	//
 	//   See also: PLOTMODEL, PLOT_MANAGER
 
-	//declare variables:  {{{
+	//{{{ declare variables:
 	var vertices = [];
 	var indices = [];
 	var texcoords = [];
 	var nanindices = {};
-	var xmin,xmax;
-	var ymin,ymax;
-	var zmin,zmax;
-	var matrixscale,vertexscale;
+	var xmin, xmax;
+	var ymin, ymax;
+	var zmin, zmax;
+	var matrixscale, vertexscale;
 
 	//Process data and model
-	var meshresults = processmesh(md,data,options);
+	var meshresults = processmesh(md, data, options);
 	var x = meshresults[0]; 
 	var y = meshresults[1]; 
@@ -28,9 +28,9 @@
 	//Compue scaling through matrices for 2d meshes and vertices for 3d meshes
 	if (!md.geometry.surface) {
-		md.geometry.surface=NewArrayFill(md.mesh.x.length,0);
+		md.geometry.surface=NewArrayFill(md.mesh.x.length, 0);
 	}
 	if (md.mesh.classname() == 'mesh3dsurface') {
 		matrixscale = 1;
-		vertexscale = options.getfieldvalue('heightscale',1);
+		vertexscale = options.getfieldvalue('heightscale', 1);
 	}
 	else {
@@ -38,5 +38,5 @@
 			z=md.geometry.surface;
 		}	
-		matrixscale = options.getfieldvalue('heightscale',1);
+		matrixscale = options.getfieldvalue('heightscale', 1);
 		vertexscale = 0;
 	}
@@ -44,10 +44,10 @@
 	
 	//Compute coordinates and data range:
-	var modelxlim = [ArrayMin(x),ArrayMax(x)];
-	var modelylim = [ArrayMin(y),ArrayMax(y)];
-	var modelzlim = [ArrayMin(z),ArrayMax(z)];
-	var xlim = options.getfieldvalue('xlim',modelxlim);
-	var ylim = options.getfieldvalue('ylim',modelylim);
-	var zlim = options.getfieldvalue('zlim',modelzlim);
+	var modelxlim = [ArrayMin(x), ArrayMax(x)];
+	var modelylim = [ArrayMin(y), ArrayMax(y)];
+	var modelzlim = [ArrayMin(z), ArrayMax(z)];
+	var xlim = options.getfieldvalue('xlim', modelxlim);
+	var ylim = options.getfieldvalue('ylim', modelylim);
+	var zlim = options.getfieldvalue('zlim', modelzlim);
 	xmin = xlim[0];
 	xmax = xlim[1];
@@ -68,10 +68,10 @@
 	node.translation = [0, 0, 0];
 	node.center = [(xmin + xmax) / 2, (ymin + ymax) / 2, (zmin + zmax) / 2];
-	node.texture = initTexture(gl,options.getfieldvalue('overlay_image'));
-	node.alpha = options.getfieldvalue('outeralpha',1.0);
+	node.texture = initTexture(gl, options.getfieldvalue('overlay_image'));
+	node.alpha = options.getfieldvalue('outeralpha', 1.0);
 	node.drawOrder = 1;
 	node.maskEnabled = options.getfieldvalue('outermask','off') == 'on';
-	node.maskHeight = options.getfieldvalue('outermaskheight',150.0);
-	node.maskColor = options.getfieldvalue('outermaskcolor',[0.0,0.0,1.0,1.0]);
+	node.maskHeight = options.getfieldvalue('outermaskheight', 150.0);
+	node.maskColor = options.getfieldvalue('outermaskcolor',[0.0, 0.0, 1.0, 1.0]);
 	updateModelMatrix(node);
 	
@@ -88,7 +88,7 @@
 		
 		//Reclaculate bounds based on otuer radaroverlay
-		modelxlim = [ArrayMin(x),ArrayMax(x)];
-		modelylim = [ArrayMin(y),ArrayMax(y)];
-		modelzlim = [ArrayMin(z),ArrayMax(z)];
+		modelxlim = [ArrayMin(x), ArrayMax(x)];
+		modelylim = [ArrayMin(y), ArrayMax(y)];
+		modelzlim = [ArrayMin(z), ArrayMax(z)];
 		xmin = xlim[0];
 		xmax = xlim[1];
@@ -159,4 +159,4 @@
 		indices[indices.length] = element[2];
 	}
-	node.mesh = GL.Mesh.load({vertices:vertices, coords:texcoords, triangles:indices}, null, null, gl);
+	node.mesh = GL.Mesh.load({vertices: vertices, coords: texcoords, triangles: indices}, null, null, gl);
 } //}}}
Index: /issm/trunk-jpl/src/m/plot/plot_quiver.js
===================================================================
--- /issm/trunk-jpl/src/m/plot/plot_quiver.js	(revision 21682)
+++ /issm/trunk-jpl/src/m/plot/plot_quiver.js	(revision 21683)
@@ -1,36 +1,39 @@
-function plot_unit(md,data,datatype,options,canvas) { //{{{
-	//PLOT_UNIT - unit plot, display data
+function plot_quiver(md, options, canvas, updateVel) { //{{{
+	//PLOT_QUIVER - quiver plot with colors
 	//
 	//   Usage:
-	//      plot_unit(md,data,options,canvas);
+	//      plot_quiver(md, options, canvas)
 	//
 	//   See also: PLOTMODEL, PLOT_MANAGER
 
-	//declare variables:  {{{
+	//{{{ declare variables:
+	var vertices = [];
+	var indices = [];
+	var colors = [];
+	var xmin, xmax;
+	var ymin, ymax;
+	var zmin, zmax;
+	var scale, matrixscale, vertexscale;
+	
 	//Process data and model
-	var meshresults = processmesh(md,data,options);
+	var meshresults = processmesh(md,[], options);
+
 	var x = meshresults[0]; 
 	var y = meshresults[1]; 
 	var z = meshresults[2]; 
-	var elements = meshresults[3];
+	var elements = meshresults[3]; 
 	var is2d = meshresults[4]; 
 	var isplanet = meshresults[5];
-	
-	var vertices = new Float32Array(x.length * 3);
-	var texcoords = new Float32Array(x.length * 2);
-	var indices = new Uint16Array(elements.length * 3);
-	var nanindices = {};
-	var xmin,xmax;
-	var ymin,ymax;
-	var zmin,zmax;
-	var datamin,datamax,datadelta;
-	var matrixscale,vertexscale;
+	var v = updateVel != undefined ? updateVel.vel : md.initialization.vel;
+	var vx = updateVel != undefined ? updateVel.vx : md.initialization.vx;
+	var vy = updateVel != undefined ? updateVel.vy : md.initialization.vy;
+		
 	//Compue scaling through matrices for 2d meshes and vertices for 3d meshes
 	if (!md.geometry.surface) {
-		md.geometry.surface=NewArrayFill(md.mesh.x.length,0);
+		md.geometry.surface=NewArrayFill(md.mesh.x.length, 0);
 	}
 	if (md.mesh.classname() == 'mesh3dsurface') {
 		matrixscale = 1;
-		vertexscale = options.getfieldvalue('heightscale',1);
+		vertexscale = options.getfieldvalue('heightscale', 1);
 	}
 	else {
@@ -38,5 +41,5 @@
 			z=md.geometry.surface;
 		}	
-		matrixscale = options.getfieldvalue('heightscale',1);
+		matrixscale = options.getfieldvalue('heightscale', 1);
 		vertexscale = 0;
 	}
@@ -44,10 +47,10 @@
 
 	//Compute coordinates and data range:
-	var modelxlim = [ArrayMin(x),ArrayMax(x)];
-	var modelylim = [ArrayMin(y),ArrayMax(y)];
-	var modelzlim = [ArrayMin(z),ArrayMax(z)];
-	var xlim = options.getfieldvalue('xlim',modelxlim);
-	var ylim = options.getfieldvalue('ylim',modelylim);
-	var zlim = options.getfieldvalue('zlim',modelzlim);
+	var modelxlim = [ArrayMin(x), ArrayMax(x)];
+	var modelylim = [ArrayMin(y), ArrayMax(y)];
+	var modelzlim = [ArrayMin(z), ArrayMax(z)];
+	var xlim = options.getfieldvalue('xlim', modelxlim);
+	var ylim = options.getfieldvalue('ylim', modelylim);
+	var zlim = options.getfieldvalue('zlim', modelzlim);
 	xmin = xlim[0];
 	xmax = xlim[1];
@@ -56,242 +59,80 @@
 	zmin = zlim[0];
 	zmax = zlim[1];
-	var caxis;
 
 	//Compute gl variables:
 	var gl = canvas.gl;
 	var node = Node(gl);
-	canvas.nodes[canvas.nodes.length] = node;
-	canvas.unitNode = node;
-	node.name = "unit";
-	node.shaderName = "Textured";
+	canvas.nodes["velocity"] = node;
+	node.name = "quiver";
+	node.shaderName = "Colored";
 	node.shader = gl.shaders[node.shaderName];
+	node.lineWidth = options.getfieldvalue('linewidth', 1);
 	node.scale = [1, 1, matrixscale];
 	node.rotation = [-90, 0, 0];
 	node.translation = [0, 0, 0];
 	node.center = [(xmin + xmax) / 2, (ymin + ymax) / 2, (zmin + zmax) / 2];
-	node.alpha = options.getfieldvalue('alpha',1.0);
-	node.drawOrder = 1;
+	node.drawMode = gl.LINES;
+	node.useIndexBuffer = false;
+	node.drawOrder = 0;
 	node.maskEnabled = options.getfieldvalue('innermask','off') == 'on';
-	node.maskHeight = options.getfieldvalue('innermaskheight',150.0);
-	node.maskColor = options.getfieldvalue('innermaskcolor',[0.0,0.0,1.0,1.0]);
-	node.enabled = options.getfieldvalue('nodata','off') == 'off';
+	node.maskHeight = options.getfieldvalue('innermaskheight', 150.0)*options.getfieldvalue('heightscale', 1);
+	node.maskColor = options.getfieldvalue('innermaskcolor',[0.0, 0.0, 1.0, 1.0]);
 	updateModelMatrix(node);
 
-	switch(datatype){
-		//element plot {{{
-		case 1:
-			pos=ArrayFindNot(data,NaN); //needed for element on water
-			if (elements[0].length==6){ //prisms
+	//retrieve some options
+	var edgecolor=new RGBColor(options.getfieldvalue('edgecolor','black'));
+	if (edgecolor.ok) edgecolor = [edgecolor.r/255.0, edgecolor.g/255.0, edgecolor.b/255.0, 1.0];
+	else throw Error(sprintf("s%s%s\n","initWebGL error message: cound not find out edgecolor color for curent canvas ", canvas));
+
+	//{{{ node plot
+	if (elements[0].length==6){ //prisms
+	}
+	else if (elements[0].length==4){ //tetras
+	}
+	else{ //2D triangular elements
+		var xyz = vec3.create();
+		var xyz = vec3.create();
+		var direction = vec3.create();
+		var vertex = vec3.create();
+		var vertexBase = vec3.create();
+		var verticesArrow = [vec3.fromValues(0.0, 0.0, 0.0), vec3.fromValues(1.0, 0.0, 0.0), vec3.fromValues(0.667, -0.167, 0.0), vec3.fromValues(1.0, 0.0, 0.0), vec3.fromValues(0.667, 0.166, 0.0), vec3.fromValues(1.0, 0.0, 0.0)];
+		var magnitude;
+		var color = edgecolor;
+		var scaling = options.getfieldvalue('scaling', 1);
+		var scale;
+		for(var i = 0; i < x.length; i++){
+			//Check for NaN values and remove from indices array as necessary, but preserve vertex array spacing
+			if (isNaN(x[i]) || isNaN(y[i]) || isNaN(z[i])) continue;
+			//Scale vertices
+			xyz = vec3.fromValues(x[i], y[i], z[i]);
+			magnitude = vec3.length(xyz) + md.geometry.surface[i] * vertexscale;
+			vec3.normalize(direction, xyz);
+			vec3.scale(vertex, direction, magnitude);
+			vec3.copy(vertexBase, vertex);
+			
+			scale = scaling*v[i];
+			var modelMatrix = mat4.create();
+			var scaleMatrix = mat4.create();
+			var rotationMatrix = mat4.create();
+			mat4.scale(scaleMatrix, scaleMatrix, vec3.fromValues(scale, scale, scale));
+			mat4.rotate(rotationMatrix, rotationMatrix, Math.atan2(vy[i], vx[i]), [0.0, 0.0, 1.0]);
+			mat4.multiply(modelMatrix, rotationMatrix, scaleMatrix);
+
+			var temp = vec3.fromValues(0.0, 0.0, 0.0);
+			for (var j = 0; j < 6; j++){
+				vec3.transformMat4(vertex, verticesArrow[j], modelMatrix);
+				vec3.add(vertex, vertex, vertexBase);
+				vertices[vertices.length] = vertex[0];
+				vertices[vertices.length] = vertex[1];
+				vertices[vertices.length] = vertex[2];
+				
+				colors[colors.length] = color[0];
+				colors[colors.length] = color[1];
+				colors[colors.length] = color[2];
+				colors[colors.length] = color[3];
 			}
-			else if (elements[0].length==4){ //tetras
-			}
-			else{ //2D triangular elements
-			}
-			break;
-		//}}}
-		//node plot {{{
-		case 2:
-			if (elements[0].length==6){ //prisms
-			}
-			else if (elements[0].length==4){ //tetras
-			}
-			else{ //triangular elements	
-				caxis = options.getfieldvalue('caxis',[ArrayMin(data),ArrayMax(data)]);
-				if (options.getfieldvalue('log','off')!='off') caxis = [Math.log10(caxis[0])/Math.log10(options.getfieldvalue('log',10)),Math.log10(caxis[1])/Math.log10(options.getfieldvalue('log',10))];
-				datamin = caxis[0];
-				datamax = caxis[1];
-				datadelta = datamax - datamin;
-
-				var xyz = vec3.create();
-				var direction = vec3.create();
-				var vertex = vec3.create();
-				var magnitude;
-
-				for(var i = 0, vindex = 0, tindex = 0; i < x.length; i++){
-					//Check for NaN values and remove from indices array as necessary, but preserve vertex array spacing
-					if (isNaN(x[i]) || isNaN(y[i]) || isNaN(z[i]) || isNaN(data[i])) {
-						nanindices[i] = i;
-						vertices[vindex++] = vertex[0];
-						vertices[vindex++] = vertex[1];
-						vertices[vindex++] = vertex[2];
-						
-						texcoords[tindex++] = 0.5;
-						texcoords[tindex++] = 0.0;
-						continue;
-					}
-
-					//Scale vertices
-					xyz = vec3.fromValues(x[i], y[i], z[i]);
-					magnitude = vec3.length(xyz) + md.geometry.surface[i] * vertexscale;
-					vec3.normalize(direction, xyz);
-					vec3.scale(vertex, direction, magnitude);
-					vertices[vindex++] = vertex[0];
-					vertices[vindex++] = vertex[1];
-					vertices[vindex++] = vertex[2];
-
-					texcoords[tindex++] = 0.5;
-					texcoords[tindex++] = clamp((data[i] - datamin) / datadelta, 0.0, 1.0);
-				}
-
-				//linearize the elements array: 
-				var element;
-				for(var i = 0, iindex = 0; i < elements.length; i++){
-					element = [elements[i][0] - 1, elements[i][1] - 1, elements[i][2] - 1];
-					if (element[0] in nanindices || element[1] in nanindices || element[2] in nanindices) continue;
-					indices[iindex++] = element[0];
-					indices[iindex++] = element[1];
-					indices[iindex++] = element[2];
-				}
-			}
-			node.mesh = GL.Mesh.load({vertices:vertices, coords:texcoords, triangles:indices}, null, null, gl);
-			node.mesh.octree = new GL.Octree(node.mesh);
-			break;
-		//}}}
-		//quiver plot {{{
-		case 3:
-			if (is2d){
-				//plot_quiver(x,y,data(:,1),data(:,2),options);
-			}
-			else{
-				//plot_quiver3(x,y,z,data(:,1),data(:,2),data(:,3),options);
-			}
-			break;
-		//}}}
-		//node transient plot {{{
-		case 5:
-			if (elements[0].length==6){ //prisms
-			}
-			else if (elements[0].length==4){//tetras
-			}
-			else{ //triangular elements
-				var xyz = vec3.create();
-				var direction = vec3.create();
-				var vertex = vec3.create();
-				var magnitude;
-				var timestamps = data[data.length-1];
-				for(var i = 0, vindex = 0, tindex = 0; i < x.length; i++){
-					//Check for NaN values and remove from indices array as necessary, but preserve vertex array spacing
-					if (isNaN(x[i]) || isNaN(y[i]) || isNaN(z[i]) || isNaN(data[i][0])) {
-						nanindices[i] = i;
-					}
-					else {
-						//Scale vertices
-						xyz = vec3.fromValues(x[i], y[i], z[i]);
-						magnitude = vec3.length(xyz) + md.geometry.surface[i] * vertexscale;
-						vec3.normalize(direction, xyz);
-						vec3.scale(vertex, direction, magnitude);
-					}
-					vertices[vindex++] = vertex[0];
-					vertices[vindex++] = vertex[1];
-					vertices[vindex++] = vertex[2];
-				}	
-				//Transpose data to obtain column addressable data matrix
-				data = data[0].map(function(col, i) { 
-					return data.map(function(row) { 
-						return row[i]
-					})
-				});
-				//Prevent evaluation of datasubarray min/max if caxis exists
-				if (options.exist('caxis')) caxis = options.getfieldvalue('caxis');
-				else caxis = [ArrayMin(data[0]),ArrayMax(data[0].slice(0,-1))];
-				if (options.getfieldvalue('log','off')!='off') caxis = [Math.log10(caxis[0])/Math.log10(options.getfieldvalue('log',10)),Math.log10(caxis[1])/Math.log10(options.getfieldvalue('log',10))];
-				//Prepare texcoords to hold array of data values
-				texcoords = [];
-				for(var i = 0; i < data.length; i++){					
-					datamin = caxis[0];
-					datamax = caxis[1];
-					datadelta = datamax - datamin;
-					//Precalculate arrays for each datasubarray, trimming off timestamp value by using x.length instead of data[i].length
-					texcoords[i] = new Float32Array(x.length * 2);
-					for(var j = 0, index = 0; j < x.length; j++){
-						texcoords[i][index++] = 0.5;
-						texcoords[i][index++] = clamp((data[i][j] - datamin) / datadelta, 0.0, 1.0);
-					}
-				}
-				
-				//linearize the elements array:
-				var element;
-				for(var i = 0, iindex = 0; i < elements.length; i++){
-					element = [elements[i][0] - 1, elements[i][1] - 1, elements[i][2] - 1];
-					if (element[0] in nanindices || element[1] in nanindices || element[2] in nanindices) continue;
-					indices[iindex++] = element[0];
-					indices[iindex++] = element[1];
-					indices[iindex++] = element[2];
-				}
-				var frame =
-				//Initialize movie loop
-				node.movieLoop = canvas.movieOptions.loop;
-				node.movieInterval = 1000 / canvas.movieOptions.fps;
-				node.movieTimestamps = timestamps;
-				node.movieLength = timestamps.length;
-				node.movieFrame = 0;
-
-				var quiverVelFrames = {};
-				for(var i=0; i < md.results.length; i++){
-					quiverVelFrames[Math.floor(md.results[i].time)] = md.results[i];
-				}
-
-				if (canvas.movieHandler) { clearInterval(canvas.movieHandler); }
-				canvas.movieHandler = setInterval(function () {
-						node.movieFrame = canvas.movieFrame;
-						if (canvas.moviePlay && canvas.movieIncrement) {
-							if (canvas.movieReverse) {
-								if (node.movieFrame == 0) {
-									if (node.movieLoop) {
-										node.movieFrame = node.movieLength - 1;
-									}
-									else {
-										toggleMoviePlay(canvas);
-									}
-								}
-								else {
-									node.movieFrame = node.movieFrame - 1;
-								}
-							}
-							else { 
-								if (node.movieFrame == node.movieLength - 1) {
-									if (node.movieLoop) {
-										node.movieFrame = 0;
-									}
-									else { 
-										toggleMoviePlay(canvas);
-									}
-								}
-								else {
-									node.movieFrame = node.movieFrame + 1;
-								}
-							}
-						}
-						if (canvas.progressBar) {
-							canvas.progressBar.val(node.movieFrame);
-							canvas.progressBar.slider('refresh');
-						}
-						if (canvas.timeLabel) { canvas.timeLabel.html(node.movieTimestamps[node.movieFrame].toFixed(0) + " " + options.getfieldvalue("movietimeunit","yr")); }
-
-						var buffer = node.mesh.getBuffer("coords");
-						buffer.data = texcoords[node.movieFrame];
-						buffer.upload(canvas.gl.DYNAMIC_DRAW);
-						node.mesh.octree = new GL.Octree(node.mesh);
-					
-						if(options.getfieldvalue('quiver') == 'data'){
-							plot_quiver(md,options,canvas, {vel:quiverVelFrames[node.movieFrame].Vel, vx:quiverVelFrames[node.movieFrame].Vx, vy:quiverVelFrames[node.movieFrame].Vy});
-
-						}
-						canvas.movieFrame = node.movieFrame;
-					}, node.movieInterval);
-				if (canvas.progressBar) {
-					canvas.movieFrame = 0;
-					canvas.progressBar.val(0);
-					canvas.progressBar.attr('max', node.movieLength-1);
-					canvas.progressBar.slider('refresh');
-				}
-				
-			}
-			node.mesh = GL.Mesh.load({vertices:vertices, coords:texcoords[0], triangles:indices}, null, null, gl);
-			node.mesh.octree = new GL.Octree(node.mesh);
-			break;
-		//}}}
-		default:
-			throw Error(sprintf("%s%i%s\n",'case ',datatype,' not supported'));
+		}
 	}
+	//}}}
+	node.mesh = GL.Mesh.load({vertices: vertices, colors: colors}, null, null, gl);
 } //}}}
Index: /issm/trunk-jpl/src/m/plot/plot_unit.js
===================================================================
--- /issm/trunk-jpl/src/m/plot/plot_unit.js	(revision 21682)
+++ /issm/trunk-jpl/src/m/plot/plot_unit.js	(revision 21683)
@@ -1,13 +1,13 @@
-function plot_unit(md,data,datatype,options,canvas) {
+function plot_unit(md, data, datatype, options, canvas) { //{{{
 	//PLOT_UNIT - unit plot, display data
 	//
 	//   Usage:
-	//      plot_unit(md,data,options,canvas);
+	//      plot_unit(md, data, options, canvas);
 	//
 	//   See also: PLOTMODEL, PLOT_MANAGER
 
-	//declare variables:  {{{
+	//{{{ declare variables: 
 	//Process data and model
-	var meshresults = processmesh(md,data,options);
+	var meshresults = processmesh(md, data, options);
 	var x = meshresults[0]; 
 	var y = meshresults[1]; 
@@ -21,16 +21,16 @@
 	var indices = new Uint16Array(elements.length * 3);
 	var nanindices = {};
-	var xmin,xmax;
-	var ymin,ymax;
-	var zmin,zmax;
-	var datamin,datamax,datadelta;
-	var matrixscale,vertexscale;
+	var xmin, xmax;
+	var ymin, ymax;
+	var zmin, zmax;
+	var datamin, datamax, datadelta;
+	var matrixscale, vertexscale;
 	//Compue scaling through matrices for 2d meshes and vertices for 3d meshes
 	if (!md.geometry.surface) {
-		md.geometry.surface=NewArrayFill(md.mesh.x.length,0);
+		md.geometry.surface=NewArrayFill(md.mesh.x.length, 0);
 	}
 	if (md.mesh.classname() == 'mesh3dsurface') {
 		matrixscale = 1;
-		vertexscale = options.getfieldvalue('heightscale',1);
+		vertexscale = options.getfieldvalue('heightscale', 1);
 	}
 	else {
@@ -38,5 +38,5 @@
 			z=md.geometry.surface;
 		}	
-		matrixscale = options.getfieldvalue('heightscale',1);
+		matrixscale = options.getfieldvalue('heightscale', 1);
 		vertexscale = 0;
 	}
@@ -44,10 +44,10 @@
 
 	//Compute coordinates and data range:
-	var modelxlim = [ArrayMin(x),ArrayMax(x)];
-	var modelylim = [ArrayMin(y),ArrayMax(y)];
-	var modelzlim = [ArrayMin(z),ArrayMax(z)];
-	var xlim = options.getfieldvalue('xlim',modelxlim);
-	var ylim = options.getfieldvalue('ylim',modelylim);
-	var zlim = options.getfieldvalue('zlim',modelzlim);
+	var modelxlim = [ArrayMin(x), ArrayMax(x)];
+	var modelylim = [ArrayMin(y), ArrayMax(y)];
+	var modelzlim = [ArrayMin(z), ArrayMax(z)];
+	var xlim = options.getfieldvalue('xlim', modelxlim);
+	var ylim = options.getfieldvalue('ylim', modelylim);
+	var zlim = options.getfieldvalue('zlim', modelzlim);
 	xmin = xlim[0];
 	xmax = xlim[1];
@@ -71,16 +71,16 @@
 	node.translation = [0, 0, 0];
 	node.center = [(xmin + xmax) / 2, (ymin + ymax) / 2, (zmin + zmax) / 2];
-	node.alpha = options.getfieldvalue('alpha',1.0);
+	node.alpha = options.getfieldvalue('alpha', 1.0);
 	node.drawOrder = 1;
 	node.maskEnabled = options.getfieldvalue('innermask','off') == 'on';
-	node.maskHeight = options.getfieldvalue('innermaskheight',150.0);
-	node.maskColor = options.getfieldvalue('innermaskcolor',[0.0,0.0,1.0,1.0]);
+	node.maskHeight = options.getfieldvalue('innermaskheight', 150.0);
+	node.maskColor = options.getfieldvalue('innermaskcolor',[0.0, 0.0, 1.0, 1.0]);
 	node.enabled = options.getfieldvalue('nodata','off') == 'off';
 	updateModelMatrix(node);
 
 	switch(datatype){
-		//element plot {{{
+		//{{{ element plot
 		case 1:
-			pos=ArrayFindNot(data,NaN); //needed for element on water
+			pos=ArrayFindNot(data, NaN); //needed for element on water
 			if (elements[0].length==6){ //prisms
 			}
@@ -91,5 +91,5 @@
 			break;
 		//}}}
-		//node plot {{{
+		//{{{ node plot
 		case 2:
 			if (elements[0].length==6){ //prisms
@@ -98,6 +98,6 @@
 			}
 			else{ //triangular elements	
-				caxis = options.getfieldvalue('caxis',[ArrayMin(data),ArrayMax(data)]);
-				if (options.getfieldvalue('log','off')!='off') caxis = [Math.log10(caxis[0])/Math.log10(options.getfieldvalue('log',10)),Math.log10(caxis[1])/Math.log10(options.getfieldvalue('log',10))];
+				caxis = options.getfieldvalue('caxis',[ArrayMin(data), ArrayMax(data)]);
+				if (options.getfieldvalue('log','off')!='off') caxis = [Math.log10(caxis[0])/Math.log10(options.getfieldvalue('log', 10)), Math.log10(caxis[1])/Math.log10(options.getfieldvalue('log', 10))];
 				datamin = caxis[0];
 				datamax = caxis[1];
@@ -145,19 +145,19 @@
 				}
 			}
-			node.mesh = GL.Mesh.load({vertices:vertices, coords:texcoords, triangles:indices}, null, null, gl);
+			node.mesh = GL.Mesh.load({vertices: vertices, coords: texcoords, triangles: indices}, null, null, gl);
 			node.mesh.octree = new GL.Octree(node.mesh);
 			break;
 		//}}}
-		//quiver plot {{{
+		//{{{ quiver plot 
 		case 3:
 			if (is2d){
-				//plot_quiver(x,y,data(:,1),data(:,2),options);
+				//plot_quiver(x, y, data(:, 1), data(:, 2), options);
 			}
 			else{
-				//plot_quiver3(x,y,z,data(:,1),data(:,2),data(:,3),options);
-			}
-			break;
-		//}}}
-		//node transient plot {{{
+				//plot_quiver3(x, y, z, data(:, 1), data(:, 2), data(:, 3), options);
+			}
+			break;
+		//}}}
+		//{{{ node transient plot
 		case 5:
 			if (elements[0].length==6){ //prisms
@@ -195,6 +195,6 @@
 				//Prevent evaluation of datasubarray min/max if caxis exists
 				if (options.exist('caxis')) caxis = options.getfieldvalue('caxis');
-				else caxis = [ArrayMin(data[0]),ArrayMax(data[0].slice(0,-1))];
-				if (options.getfieldvalue('log','off')!='off') caxis = [Math.log10(caxis[0])/Math.log10(options.getfieldvalue('log',10)),Math.log10(caxis[1])/Math.log10(options.getfieldvalue('log',10))];
+				else caxis = [ArrayMin(data[0]), ArrayMax(data[0].slice(0,-1))];
+				if (options.getfieldvalue('log','off')!='off') caxis = [Math.log10(caxis[0])/Math.log10(options.getfieldvalue('log', 10)), Math.log10(caxis[1])/Math.log10(options.getfieldvalue('log', 10))];
 				//Prepare texcoords to hold array of data values
 				texcoords = [];
@@ -222,10 +222,10 @@
 			
 				//Initialize movie loop
-				node.movieLoop = canvas.movieOptions.loop;
-				node.movieInterval = 1000 / canvas.movieOptions.fps;
+				node.movieLoop = canvas.animation.loop;
+				node.movieInterval = 1000 / canvas.animation.fps;
 				node.movieTimestamps = timestamps;
 				node.movieLength = timestamps.length;
 				node.movieFrame = 0;
-				canvas.dataArray = [];
+				canvas.dataMarkers.values = [];
 				var quiverVelFrames = {};
 				for(var i=0; i < md.results.length; i++){
@@ -233,59 +233,48 @@
 				}
 
-				if (canvas.movieHandler) { clearInterval(canvas.movieHandler); }
-				canvas.movieHandler = setInterval(function () {
-						node.movieFrame = canvas.movieFrame;
-						if (canvas.moviePlay && canvas.movieIncrement) {
-							if (canvas.movieReverse) {
-								if (node.movieFrame == 0) {
-									if (node.movieLoop) {
-										node.movieFrame = node.movieLength - 1;
-									}
-									else {
-										toggleMoviePlay(canvas);
-									}
-								}
-								else {
-									node.movieFrame = node.movieFrame - 1;
-								}
+				if (canvas.animation.handler !== 0) {
+					console.log("clearing...");
+					clearInterval(canvas.animation.handler)
+				}
+				//TODO: Move this into webgl.js
+				canvas.animation.handler = setInterval(function () {
+					node.movieFrame = canvas.animation.frame;
+					if (canvas.animation.play && canvas.animation.increment) {
+						if (node.movieFrame == node.movieLength - 1) {
+							if (node.movieLoop) {
+								node.movieFrame = 0;
 							}
 							else { 
-								if (node.movieFrame == node.movieLength - 1) {
-									if (node.movieLoop) {
-										node.movieFrame = 0;
-									}
-									else { 
-										toggleMoviePlay(canvas);
-									}
-								}
-								else {
-									node.movieFrame = node.movieFrame + 1;
-								}
+								toggleMoviePlay(canvas);
 							}
 						}
-						if (canvas.progressBar) {
-							canvas.progressBar.val(node.movieFrame);
-							canvas.progressBar.slider('refresh');
+						else {
+							node.movieFrame = node.movieFrame + 1;
 						}
-						if (canvas.timeLabel) { canvas.timeLabel.html(node.movieTimestamps[node.movieFrame].toFixed(0) + " " + options.getfieldvalue("movietimeunit","yr")); }
-
-						var buffer = node.mesh.getBuffer("coords");
-						buffer.data = texcoords[node.movieFrame];
-						buffer.upload(canvas.gl.DYNAMIC_DRAW);
-						node.mesh.octree = new GL.Octree(node.mesh);
-						node.texcoords = texcoords;
-						if(options.getfieldvalue('quiver') == 'on'){
-							plot_quiver(md,options,canvas, {vel:quiverVelFrames[node.movieFrame].Vel, vx:quiverVelFrames[node.movieFrame].Vx, vy:quiverVelFrames[node.movieFrame].Vy});
-
+						if (canvas.animation.lastFrame != canvas.animation.frame) {
+							updateMarker(canvas, false);
 						}
-						canvas.movieFrame = node.movieFrame;
-
-						if (canvas.moviePlay || canvas.lastMovieFrame != canvas.movieFrame) {
-							updatePlot(true);
-						}
-
-					}, node.movieInterval);
+					}
+					
+					if (canvas.progressBar) {
+						canvas.progressBar.val(node.movieFrame);
+						canvas.progressBar.slider('refresh');
+					}
+					if (canvas.timeLabel) { canvas.timeLabel.html(node.movieTimestamps[node.movieFrame].toFixed(0) + " " + options.getfieldvalue("movietimeunit","yr")); }
+					
+					var buffer = node.mesh.getBuffer("coords");
+					buffer.data = texcoords[node.movieFrame];
+					buffer.upload(canvas.gl.DYNAMIC_DRAW);
+					node.mesh.octree = new GL.Octree(node.mesh);
+					node.texcoords = texcoords;
+					if(options.getfieldvalue('quiver') == 'on'){
+						plot_quiver(md, options, canvas, {vel: quiverVelFrames[node.movieFrame].Vel, vx: quiverVelFrames[node.movieFrame].Vx, vy: quiverVelFrames[node.movieFrame].Vy});
+					}
+					canvas.animation.lastFrame = canvas.animation.frame;
+					canvas.animation.frame = node.movieFrame;					
+				}, node.movieInterval);
+				
 				if (canvas.progressBar) {
-					canvas.movieFrame = 0;
+					canvas.animation.frame = 0;
 					canvas.progressBar.val(0);
 					canvas.progressBar.attr('max', node.movieLength-1);
@@ -294,10 +283,10 @@
 				
 			}
-			node.mesh = GL.Mesh.load({vertices:vertices, coords:texcoords[0], triangles:indices}, null, null, gl);
+			node.mesh = GL.Mesh.load({vertices: vertices, coords: texcoords[0], triangles: indices}, null, null, gl);
 			node.mesh.octree = new GL.Octree(node.mesh);
 			break;
 		//}}}
 		default:
-			throw Error(sprintf("%s%i%s\n",'case ',datatype,' not supported'));
-	}
-}
+			throw Error(sprintf("%s%i%s\n",'case ', datatype,' not supported'));
+	}
+} //}}}
Index: /issm/trunk-jpl/src/m/plot/slider.js
===================================================================
--- /issm/trunk-jpl/src/m/plot/slider.js	(revision 21682)
+++ /issm/trunk-jpl/src/m/plot/slider.js	(revision 21683)
@@ -63,10 +63,5 @@
 		callback(parseFloat($(selector).val()));
 	});
-
-	/*
-		NOTE:	Slider must be "refreshed" after any JavaScript change to it, as
-				it is an AJAX object.
-	*/
-	$(selector).slider('refresh');
+	$(selector).slider('refresh'); //Slider must be "refreshed" after any JavaScript change to it, as it is an AJAX object.
 } //}}}
 
@@ -89,10 +84,5 @@
 	*/
 	$(selector).appendTo(selector + '-value');
-	
-	/*
-		NOTE:	Slider must be "refreshed" after any JavaScript change to it, as
-				it is an AJAX object.
-	*/
-	$(selector).slider('refresh');
+	$(selector).slider('refresh'); //Slider must be "refreshed" after any JavaScript change to it, as it is an AJAX object.
 } //}}}
 
@@ -130,37 +120,21 @@
 	$(progressBar).attr('max', 1);
 	$(progressBar).attr('step', 1);
-	$(progressBar).on('slidestop', function(event, ui){
-		canvas.movieIncrement = true;
-		canvas.movieFrame = parseInt($(progressBar).val());
+	$(progressBar).on('slidestart', function(event, ui){
+		onSlideStart(canvas, progressBar);
 	});
 	$(progressBar).on('change', function(event, ui){
-		canvas.movieFrame = parseInt($(progressBar).val());
+		onSlideChange(canvas, progressBar);
 	});
-	$(progressBar).on('slidestart', function(event, ui){
-		canvas.movieIncrement = false;	
-		canvas.movieFrame = parseInt($(progressBar).val());
+	$(progressBar).on('slidestop', function(event, ui){
+		onSlideStop(canvas, progressBar);
 	});
-
-	/*
-		NOTE:	Slider must be "refreshed" after any JavaScript change to it, as
-				it is an AJAX object.
-	*/
-	$(progressBar).slider('refresh');
-	
-	// Attach progress bar slider to simulation.
-	canvas.progressBar = progressBar;
+	$(progressBar).slider('refresh'); //Slider must be "refreshed" after any JavaScript change to it, as it is an AJAX object.	
 
 	playButton.click(function(){
-		canvas.moviePlay = !canvas.moviePlay;
-		if (canvas.moviePlay){
-			playButton.find("span").removeClass("fa-play");
-			playButton.find("span").addClass("fa-pause");
-		}
-		else{
-			playButton.find("span").removeClass("fa-pause");
-			playButton.find("span").addClass("fa-play");
-		}
+		toggleMoviePlay(canvas);
 	});
 	
+	canvas.progressBar = progressBar;
+	canvas.playButton = playButton;
 	canvas.timeLabel = timeText;
 } //}}}
Index: /issm/trunk-jpl/src/m/plot/webgl.js
===================================================================
--- /issm/trunk-jpl/src/m/plot/webgl.js	(revision 21682)
+++ /issm/trunk-jpl/src/m/plot/webgl.js	(revision 21683)
@@ -1,108 +1,142 @@
 /*This is where we have all our webgl relevant functionality for the plotting routines: */
-
-/*This is where we have all our webgl relevant functionality for the plotting routines: */
-
 //{{{ Canvas Initialization
 function initCanvas(options) {
-	//Initialize open Gl for each canvas, if needed: 
+	//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) {
 		typedArraySliceSupport();
-		canvas.gl = initWebGL(canvas,options);
+		if (!isEmptyOrUndefined(canvas.draw) && canvas.draw.handler !== 0)	{ window.cancelAnimationFrame(canvas.draw.handler); }
+		if (!isEmptyOrUndefined(canvas.animation) && canvas.animation.handler !== 0) { clearInterval(canvas.animation.handler); }
+		initWebGL(canvas, options);
+		initializeMarker(canvas);
 		canvas.nodes = [];
-		if (canvas.drawHandler)	{ window.cancelAnimationFrame(canvas.drawHandler); }
-		draw(canvas,options);
+		draw(canvas);
 		canvas.initialized = true;
 	}
 	return canvas;
 }
-function initWebGL(canvas,options) { //{{{
+function initWebGL(canvas, options) { //{{{
+	//Initialize canvas.gl on page load, reusing gl context on additional runs
 	var gl;
-	try {
-		if (!canvas.gl) {
-			gl = GL.create({canvas:canvas});
-			// Enable depth testing
-			gl.enable(gl.DEPTH_TEST);
-			// Near things obscure far things
-			gl.depthFunc(gl.LEQUAL);
-			// Enable color blending/overlay
-			gl.enable(gl.BLEND);
-			// Enable face culling
-			gl.enable(gl.CULL_FACE); 
-			gl.cullFace(gl.FRONT);
-			// Load shaders and store them in gl object
-			gl.shaders = loadShaders(gl,options.getfieldvalue('rootpath','../../../js/'));
-			
-			// Add event listeners for canvas
-			var displayview = options.getfieldvalue('displayview','off') == 'on';
-			var displayzoom = options.getfieldvalue('displayzoom','off') == 'on';
-			var mc = new Hammer.Manager(canvas);
-			
-			mc.add( new Hammer.Tap({ event: 'singletap' }) );
-			mc.add(new Hammer.Pan({threshold:0, pointers:0}));
-			mc.add(new Hammer.Pinch({threshold:0})).recognizeWith(mc.get('pan'));
-			mc.on('singletap', 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('mousemove', function (ev) {onTap(ev,canvas);}, false);
-			canvas.addEventListener('mousewheel', function (ev) {onZoom(ev,canvas,displayzoom)}, false);
-			canvas.addEventListener('DOMMouseScroll', function (ev) {onZoom(ev,canvas,displayzoom)}, false);
-		}
-		else {
-			gl = canvas.gl;
-		}
-	}
-	catch(e) {
-		console.log(e);
-		return;
+	if (!canvas.gl) {
+		gl = GL.create({canvas: canvas});
+		gl.enable(gl.DEPTH_TEST); // Enable depth testing
+		gl.depthFunc(gl.LEQUAL); // Near things obscure far things
+		gl.enable(gl.BLEND); // Enable color blending/overlay
+		gl.enable(gl.CULL_FACE); // Enable face culling
+		gl.cullFace(gl.FRONT);
+		gl.shaders = loadShaders(gl, options.getfieldvalue('rootpath', '../../../js/')); // Load shaders and store them in gl object
+		gl.textures = {};
+		
+		// Add event listeners for canvas
+		var displayview = options.getfieldvalue('displayview', 'off') == 'on';
+		var displayzoom = options.getfieldvalue('displayzoom', 'off') == 'on';
+		var mc = new Hammer.Manager(canvas);
+		
+		mc.add( new Hammer.Tap({event: 'singletap' }) );
+		mc.add(new Hammer.Pan({threshold: 0, pointers: 0}));
+		mc.add(new Hammer.Pinch({threshold: 0})).recognizeWith(mc.get('pan'));
+		mc.on('singletap', 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);
+	}
+	else {
+		gl = canvas.gl;
 	}
 	
 	// Add context state variables
-	//TODO:Group variables in objects for organization and naming
 	canvas.gl = gl;
-	canvas.rootPath = options.getfieldvalue('rootpath','../../../js/');
-	canvas.brush = options.getfieldvalue('brush',{'enabled':'off','strength':0.075,'falloff':0.5});
-	canvas.cameraPosition = vec3.create();
-	canvas.cameraMatrix = mat4.create();
-	canvas.controlSensitivity = options.getfieldvalue('controlsensitivity',1);
-	canvas.dataArray = [];
-	canvas.dataMarkersAllowed = options.getfieldvalue('datamarkers','off') == 'on';
-	canvas.dataMarkersEnabled = true; //if data marker feature is on, user can toggle feature on and off
-	canvas.dataMarkerImage = options.getfieldvalue('datamarkers_image',canvas.rootPath+'textures/data_marker.svg');
-	canvas.dataMarkerSize = options.getfieldvalue('datamarkerssize',[32,32]);
-	canvas.dataMarkerOptions = options.getfieldvalue('datamarkersoptions',{'enabled':'on','image':canvas.rootPath+'textures/data_marker.svg','size':[32,32],'format':['X: %.2e<br>Y: %.2e<br>Z: %.2e]<br>Value: %0.1f','x','y','z','value'],'animated':false});
-	canvas.inverseCameraMatrix = mat4.create();
-	canvas.id = options.getfieldvalue('canvasid','.sim-canvas');
-	canvas.movieFrame = 0;
-	canvas.moviePlay = true;
-	canvas.movieReverse = false;
-	canvas.movieIncrement = true;
-	canvas.movieOptions = options.getfieldvalue('movieoptions',{'fps':4,'loop':true});
-	canvas.moviefps = options.getfieldvalue('moviefps',5);
-	canvas.rotation = options.getfieldvalue('view',[0,90]); //0 azimuth, 90 elevation
-	canvas.rotationAzimuthBounds = options.getfieldvalue('azlim',[0,360]);
-	canvas.rotationElevationBounds = options.getfieldvalue('ellim',[-180,180]);
-	canvas.translation = options.getfieldvalue('origin',[0,0,0]);
-	canvas.twod = options.getfieldvalue('2d','off') == 'on';
-	canvas.view = options.getfieldvalue('view',[0,90]);
-	canvas.viewPanning = options.getfieldvalue('enablepanning','off') == 'on';
-	canvas.vInverseMatrix = mat4.create();
-	canvas.zoomBounds = options.getfieldvalue('zoomlim',[0.001,100.0]);
-	canvas.zoom = clamp(options.getfieldvalue('zoom',1.0), canvas.zoomBounds[0], canvas.zoomBounds[1]);
-	canvas.zoomLast = canvas.zoom;
-	var backgroundcolor = new RGBColor(options.getfieldvalue('backgroundcolor','lightcyan'));
+	canvas.textcanvas = null;
+	canvas.overlaycanvas = null;
+	canvas.unitNode = {};
+	canvas.unitData = {};
+	canvas.controlSensitivity = options.getfieldvalue('controlsensitivity', 1);
+	canvas.id = options.getfieldvalue('canvasid', '.sim-canvas');
+	canvas.rootPath = options.getfieldvalue('rootpath', '../../../js/');
+	canvas.selector = $('#' + canvas.id);
+	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 curent canvas ',canvas)); }
-
+	else { throw Error(sprintf('s%s%s\n','initWebGL error message: cound not find out background color for curent canvas ', canvas)); }
+	
+	//Property intiialization, using values from options first, then from default values.
+	var animation = options.getfieldvalue('movies', {});
+	canvas.animation = {
+		frame: defaultFor(animation.frame, 0),
+		play: defaultFor(animation.play, true),
+		increment: defaultFor(animation.increment, true),
+		fps: defaultFor(animation.fps, 4),
+		loop: defaultFor(animation.loop, true),
+		handler: defaultFor(animation.handler, 0)
+	}
+	var brush = options.getfieldvalue('brush', {});
+	canvas.brush = {
+		enabled: defaultFor(brush.enabled, false),
+		strength: defaultFor(brush.strength, 0.075),
+		falloff: defaultFor(brush.falloff, 0.5),
+		hit: defaultFor(brush.hit, {})
+	};
+	var camera = options.getfieldvalue('camera', {});
+	canvas.camera = {
+		position: defaultFor(camera.position, vec3.create()),
+		rotation: defaultFor(camera.rotation, vec3.create()),
+		near: defaultFor(camera.near, 1e3),
+		far: defaultFor(camera.far, 1e10),
+		fov: defaultFor(camera.fov, 45),
+		vMatrix: defaultFor(camera.vMatrix, mat4.create()),
+		pMatrix: defaultFor(camera.pMatrix, mat4.create()),
+		vpMatrix: defaultFor(camera.vpMatrix, mat4.create()),
+		vInverseMatrix: defaultFor(camera.vInverseMatrix, mat4.create()),
+		pInverseMatrix: defaultFor(camera.pInverseMatrix, mat4.create()),
+		vpInverseMatrix: defaultFor(camera.vpInverseMatrix, mat4.create()),
+		ready: defaultFor(camera.ready, false)
+	};
+	var clouds = options.getfieldvalue('clouds', {});
+	canvas.clouds = {
+		enabled: defaultFor(clouds.enabled, false),
+		height: defaultFor(clouds.height, 7500),
+		quantity: defaultFor(clouds.quantity, 10)
+	};
+	var dataMarkers = options.getfieldvalue('datamarkers', {});
+	canvas.dataMarkers = {
+		enabled: defaultFor(dataMarkers.enabled, true),
+		values: defaultFor(dataMarkers.values, []),
+		image: defaultFor(dataMarkers.image, canvas.rootPath+'textures/data_marker.svg'),
+		size: defaultFor(dataMarkers.size, [32, 32]),
+		format: defaultFor(dataMarkers.format, ['X: %.2e<br>Y: %.2e<br>Z: %.2e<br>Value: %0.1f', 'x', 'y', 'z', 'value']),
+		animated: defaultFor(dataMarkers.animated, false),
+		labels: defaultFor(dataMarkers.labels, []),
+		font: defaultFor(dataMarkers.font, ''),
+		marker: defaultFor(dataMarkers.marker, document.getElementById('sim-data-marker-' + canvas.id))
+	};
+	var draw = options.getfieldvalue('draw', {});
+	canvas.draw = {
+		ready: defaultFor(draw.ready, false),
+		handler: defaultFor(draw.handler, null)
+	};
+	var view = options.getfieldvalue('view', {});
+	canvas.view = {
+		position: defaultFor(view.position, [0.0, 0.0, 0.0]),
+		rotation: defaultFor(view.rotation, [0, 90]),
+		zoom: defaultFor(view.zoom, 1.0),
+		zoomLimits: defaultFor(view.zoomLimits, [0.001, 100.0]),
+		lastZoom: defaultFor(view.lastZoom, 1.0),
+		azimuthLimits: defaultFor(view.azimuthLimits, [0, 360]),
+		elevationLimits: defaultFor(view.elevationLimits, [-180, 180]),
+		panningEnabled: defaultFor(view.panningEnabled, false),
+		twod: defaultFor(view.twod, false)
+	};
+
+	//Override with parameters from URL, if any
 	//TODO: Make permalinks more robust and less interdependent on UI
-	//Override with parameters from URL, if any
 	if (!canvas.usedparemters) {
 		function getJsonFromUrl() {
 			var query = location.search.substr(1);
 			var result = {};
-			query.split("&").forEach(function(part) {
-				var item = part.split("=");
+			query.split('&').forEach(function(part) {
+				var item = part.split('=');
 				result[item[0]] = decodeURIComponent(item[1]);
 			});
@@ -111,15 +145,9 @@
 		parameters = getJsonFromUrl();
 		
-		if (parameters["rotation"]) {
-			canvas.rotation = JSON.parse(parameters["rotation"]);
-		}
-		if (parameters["view"]) {
-			canvas.view = JSON.parse(parameters["view"]);
-		}
-		if (parameters["zoom"]) {
-			canvas.zoom = JSON.parse(parameters["zoom"]);
-		}
-		if (parameters["initial"]) {
-			initial = JSON.parse(parameters["initial"]);
+		if (parameters['view']) {
+			canvas.view = JSON.parse(parameters['view']);
+		}
+		if (parameters['initial']) {
+			initial = JSON.parse(parameters['initial']);
 			if (!initial) {
 				if (typeof SolveGlacier == 'function') {
@@ -133,12 +161,10 @@
 		canvas.usedparemters = true;
 	}
-
-	return gl;
 } //}}}
 function generatePermalink() { //{{{
-	var permalink = window.location.origin + window.location.pathname + "?rotation=" + JSON.stringify(canvas.rotation) + "&view=" + JSON.stringify(canvas.view) + "&zoom=" + JSON.stringify(canvas.zoom) + "&initial=" + JSON.stringify(initial);
-	window.prompt("Share this simulation: ", permalink);
-} //}}}
-function loadShaders(gl,rootPath) { //{{{
+	var permalink = window.location.origin + window.location.pathname + '&view=' + JSON.stringify(canvas.view) + '&initial=' + JSON.stringify(initial);
+	window.prompt('Share this simulation: ', permalink);
+} //}}}
+function loadShaders(gl, rootPath) { //{{{
 	var shaders = {};
 	shaders.Colored = new GL.Shader.fromURL(rootPath+'shaders/Colored.vsh', rootPath+'shaders/Colored.fsh', null, gl);
@@ -146,44 +172,49 @@
 	shaders.SkyFromSpace = new GL.Shader.fromURL(rootPath+'shaders/SkyFromSpace.vert', rootPath+'shaders/SkyFromSpace.frag', null, gl);
 	shaders.GroundFromSpace = new GL.Shader.fromURL(rootPath+'shaders/GroundFromSpace.vert', rootPath+'shaders/GroundFromSpace.frag', null, gl);
+	shaders.Clouds = new GL.Shader.fromURL(rootPath+'shaders/Clouds.vert', rootPath+'shaders/Clouds.frag', null, gl);
 	return shaders;
 } //}}}
-function initTexture(gl,imageSource) { //{{{
-	return GL.Texture.fromURL(imageSource, {minFilter:gl.LINEAR_MIPMAP_LINEAR, magFilter:gl.LINEAR}, null, gl);
+function initTexture(gl, imageSource) { //{{{
+	//Initialize textures, or load from memory if they already exist.
+	if (isEmptyOrUndefined(gl.textures[imageSource])) {
+		gl.textures[imageSource] = GL.Texture.fromURL(imageSource, {minFilter: gl.LINEAR_MIPMAP_LINEAR, magFilter: gl.LINEAR}, null, gl);
+	}
+	return gl.textures[imageSource];
 } //}}}
 function Node(gl) { //{{{
 	//Returns a Node object that contains default display states for webgl object. center represents pivot point of rotation.
 	return {
-		alpha:1.0,
-		buffers:[],
-		cullFace:gl.BACK,
-		disableDepthTest:false, 
-		drawMode:gl.TRIANGLES,
-		drawOrder:0,
-		enabled:true,
-		enableCullFace:true,
-		hideOcean:false,
-		lineWidth:1.0,
-		maskEnabled:false,
-		maskHeight:150.0,
-		maskColor:vec4.fromValues(0.0, 0.0, 1.0, 1.0),
-		mesh:null,
-		name:'node',
-		shaderName:'Colored',
-		shader:gl.shaders.Colored,
-		texture:null,
-		useIndexBuffer:true,
-		center:vec3.create(), 
-		scale:vec3.fromValues(1, 1, 1),
-		rotation:vec3.create(),
-		translation:vec3.create(),
-		modelMatrix:mat4.create(),
-		rotationMatrix:mat4.create(),
-		inverseModelMatrix:mat4.create(),
-		inverseRotationMatrix:mat4.create()
+		alpha: 1.0,
+		buffers: [],
+		cullFace: gl.BACK,
+		disableDepthTest: false, 
+		drawMode: gl.TRIANGLES,
+		drawOrder: 0,
+		enabled: true,
+		enableCullFace: true,
+		hideOcean: false,
+		lineWidth: 1.0,
+		maskEnabled: false,
+		maskHeight: 150.0,
+		maskColor: vec4.fromValues(0.0, 0.0, 1.0, 1.0),
+		mesh: null,
+		name: 'node',
+		shaderName: 'Colored',
+		shader: gl.shaders.Colored,
+		texture: null,
+		useIndexBuffer: true,
+		center: vec3.create(), 
+		scale: vec3.fromValues(1, 1, 1),
+		rotation: vec3.create(),
+		translation: vec3.create(),
+		modelMatrix: mat4.create(),
+		rotationMatrix: mat4.create(),
+		inverseModelMatrix: mat4.create(),
+		inverseRotationMatrix: mat4.create()
 	};
 } //}}}
 function debugNodes(canvasid) { //{{{
 	var canvasid = canvasid || '.sim-canvas';
-	var nodes = $(canvasid)[0].nodes;
+	var nodes = document.getElementById(canvasid).nodes;
 	console.log(canvasid, 'Nodes:');
 	for (var node in nodes) {
@@ -196,5 +227,5 @@
 
 	var translationMatrix = mat4.create();
-	mat4.translate(translationMatrix, translationMatrix, [-node.center[0],-node.center[1],-node.center[2]]); //scale/rotation centering
+	mat4.translate(translationMatrix, translationMatrix, vec3.negate(vec3.create(), node.center)); //scale/rotation centering
 	mat4.multiply(modelMatrix, translationMatrix, modelMatrix);
 	
@@ -226,13 +257,24 @@
 	node.inverseModelMatrix = mat4.invert(mat4.create(), modelMatrix);
 	node.rotationMatrix = rotationMatrix;
-	node.inverseRotationMatrix = mat4.invert(mat4.create(), rotationMatrix);;
+	node.inverseRotationMatrix = mat4.invert(mat4.create(), rotationMatrix);
 } //}}}
 function clamp(value, min, max) { //{{{
 	return Math.max(min, Math.min(value, max));
 } //}}}
-function recover(canvasid,name,defaultvalue) { //{{{
-	var canvas = document.getElementById(canvasid);
-	if (canvas && canvas.hasOwnProperty(name)) { return canvas[name]; }
-	return defaultvalue;
+function defaultFor(name, value) { //{{{
+	return typeof name !== 'undefined' ? name : value;
+} //}}}
+function isEmptyOrUndefined(object) { //{{{
+	return object === undefined || Object.getOwnPropertyNames(object).length === 0;
+} //}}}
+function recover(canvasid, name, value) { //{{{
+	//Traverse canvas object tree for property defined by dot delimited string, returning it, or a default value if it is not found.
+	var object = document.getElementById(canvasid);
+	var properties = name.split('.');
+	for (var i = 0; i < properties.length; ++i) {
+		object = object[properties[i]];
+		if (typeof object === 'undefined') { break; }
+    }
+	return defaultFor(object, value);
 } //}}}
 function typedArraySliceSupport() { //{{{
@@ -279,24 +321,4 @@
 	}
 } //}}}
-function raycast(canvas, x, y) { //{{{
-	var inverseMVPMatrix = mat4.invert(mat4.create(), mat4.multiply(mat4.create(), canvas.cameraMatrix, canvas.unitNode.modelMatrix));
-	var origin = origin || vec3.transformMat4(vec3.create(), [(x - canvas.width / 2) / (canvas.width / 2), (canvas.height / 2 - y) / (canvas.height / 2), 0], inverseMVPMatrix);
-	var far = far || vec3.transformMat4(vec3.create(), [(x - canvas.width / 2) / (canvas.width / 2), (canvas.height / 2 - y) / (canvas.height / 2), 1.0], inverseMVPMatrix);
-	var ray = vec3.subtract(vec3.create(), far, origin);
-
-	var mesh = canvas.unitNode.mesh;
-	if (!mesh || mesh.ready == false) { return; }
-	if (!mesh.octree) { mesh.octree = new GL.Octree(mesh); }
-	
-	var hit = mesh.octree.testRay(origin, ray, 1e3, 1e10);
-	
-	if(!hit) { return; }
-	
-	hit.modelPos = vec3.copy(vec3.create(), hit.pos);
-	vec3.transformMat4(hit.pos, hit.pos, canvas.unitNode.modelMatrix);
-	vec3.transformMat4(hit.normal, hit.normal, canvas.unitNode.modelMatrix);
-
-	return hit;
-} //}}}
 //}}}
 //{{{ Interface Functions
@@ -304,9 +326,134 @@
 	//Sets up a marker on a canvas that will track a point on the mesh. Can be dismissed by closing the display or clicking the marker.
 	ev.preventDefault();
-	if (!(canvas.dataMarkersAllowed && canvas.dataMarkersEnabled)) { return; }
-	initializeMarker(canvas, ev.srcEvent.layerX, ev.srcEvent.layerY, true);
-	brushModify(canvas, ev.srcEvent.layerX, ev.srcEvent.layerY);
-} //}}}
-function brushModify(canvas, x, y) { //{{{
+	if (!canvas.dataMarkers.enabled) { return; }
+	var hit = raycast(canvas, ev.srcEvent.layerX, ev.srcEvent.layerY);
+	canvas.dataMarkers.marker.hit = hit;
+	canvas.brush.hit = hit;
+	updateMarker(canvas, true);
+	brushModify(canvas);
+} //}}}
+function onPan(ev, canvas, displaylog) { //{{{
+	ev.preventDefault();
+	
+	if (canvas.dataMarkers.enabled == 'on') {
+		canvas.brush.hit = raycast(canvas, ev.srcEvent.layerX, ev.srcEvent.layerY);
+		brushModify(canvas);
+	}
+	
+	if (ev.type == 'panstart') {
+		canvas.lastDeltaX = 0;
+		canvas.lastDeltaY = 0;
+	}
+	if (ev.srcEvent.shiftKey || ev.pointers.length == 2) {
+		if (!canvas.view.panningEnabled) return;
+		var deltaX = (canvas.lastDeltaX - ev.deltaX) / canvas.clientWidth / canvas.view.zoom * 2 * canvas.controlSensitivity * 6.371e6;
+		var deltaY = (canvas.lastDeltaY - ev.deltaY) / canvas.clientHeight / canvas.view.zoom * 2 * canvas.controlSensitivity * 6.371e6;
+		
+		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;
+		}
+		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;
+		}
+	}
+	
+	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;
+		
+		if (canvas.view.rotation[0] > 360) { canvas.view.rotation[0] -= 360; };
+		if (canvas.view.rotation[0] < -360) { canvas.view.rotation[0] += 360; };
+		if (canvas.view.rotation[1] > 180) { canvas.view.rotation[1] -= 360; };
+		if (canvas.view.rotation[1] < -180) { canvas.view.rotation[1] += 360; };
+		
+		canvas.view.rotation[0] = clamp(canvas.view.rotation[0], canvas.view.azimuthLimits[0], canvas.view.azimuthLimits[1]);
+		canvas.view.rotation[1] = clamp(canvas.view.rotation[1], canvas.view.elevationLimits[0], canvas.view.elevationLimits[1])
+	}
+	canvas.lastDeltaX = ev.deltaX;
+	canvas.lastDeltaY = ev.deltaY;
+	
+	repositionMarker(canvas);
+	
+	if (displaylog) { console.log(canvas.view.rotation); }
+} //}}}
+function onPinch(ev, canvas, displaylog) { //{{{
+	ev.preventDefault();
+	if (ev.type == 'pinchstart') { canvas.view.lastZoom = canvas.view.zoom; }
+	else { modifyZoom(ev.scale * canvas.view.lastZoom, canvas, displaylog); }
+} //}}}
+function onZoom(ev, canvas, displaylog) { //{{{
+	ev.preventDefault();
+	var delta = clamp(ev.scale || ev.wheelDelta || -ev.detail, -1, 1) * canvas.controlSensitivity * canvas.view.zoom / 20;
+	modifyZoom(canvas.view.zoom + delta, canvas, displaylog);
+} //}}}
+function modifyZoom(value, canvas, displaylog) { //{{{
+	canvas.view.zoom = clamp(value, canvas.view.zoomLimits[0], canvas.view.zoomLimits[1]);
+	repositionMarker(canvas);
+	if (displaylog) { console.log(canvas.view.zoom); }
+} //}}}
+function modifyDataMarkersEnabled(value, canvas) { //{{{
+	canvas.dataMarkers.enabled = value;
+} //}}}
+function toggleMoviePlay(canvas) { //{{{
+	canvas.animation.play = !canvas.animation.play;
+	if (canvas.animation.play){
+		canvas.playButton.find('span').removeClass('fa-play');
+		canvas.playButton.find('span').addClass('fa-pause');
+	}
+	else{
+		canvas.playButton.find('span').removeClass('fa-pause');
+		canvas.playButton.find('span').addClass('fa-play');
+	}
+} //}}}
+function onSlideStart(canvas, progressBar) { //{{{
+	if (!isEmptyOrUndefined(canvas.animation)) {
+		canvas.animation.increment = false;	
+		canvas.animation.frame = parseInt($(progressBar).val());
+		//console.log(canvas.animation.frame);
+		//updateMarker(canvas, false);
+	}
+} //}}}
+function onSlideChange(canvas, progressBar) { //{{{
+	if (!isEmptyOrUndefined(canvas.animation)) {
+		canvas.animation.frame = parseInt($(progressBar).val());
+		//console.log("change");
+		updateMarker(canvas, false);
+	}
+} //}}}
+function onSlideStop(canvas, progressBar) { //{{{
+	if (!isEmptyOrUndefined(canvas.animation)) {
+		canvas.animation.increment = true;	
+		canvas.animation.frame = parseInt($(progressBar).val());
+		//console.log(canvas.animation.frame);
+		//updateMarker(canvas, false);
+	}
+} //}}}
+//}}}
+//{{{ Interaction Functions
+function raycast(canvas, x, y) { //{{{
+	//Performs raycast on canvas.unitNode.mesh using x/y screen coordinates. Returns hit objects with hit position, coords, and indicies of ray-triangle intersection.
+	//TODO: Diagnose marker issues with orthographic views and slr-eustatic updates when switching between basins.
+	var inverseMVPMatrix = mat4.invert(mat4.create(), mat4.multiply(mat4.create(), canvas.camera.vpMatrix, canvas.unitNode.modelMatrix));
+	var origin = vec3.transformMat4(vec3.create(), [(x - canvas.width / 2) / (canvas.width / 2), (canvas.height / 2 - y) / (canvas.height / 2), 0], inverseMVPMatrix);
+	var far = far || vec3.transformMat4(vec3.create(), [(x - canvas.width / 2) / (canvas.width / 2), (canvas.height / 2 - y) / (canvas.height / 2), 1.0], inverseMVPMatrix);
+	var ray = vec3.subtract(vec3.create(), far, origin);
+
+	var mesh = canvas.unitNode.mesh;
+	if (!mesh) { return; }
+	if (!mesh.octree) { mesh.octree = new GL.Octree(mesh); }
+	
+	var hit = mesh.octree.testRay(origin, ray, 1e3, 1e10);
+	
+	if(!hit) { return; }
+	
+	hit.modelPos = vec3.copy(vec3.create(), hit.pos);
+	vec3.transformMat4(hit.pos, hit.pos, canvas.unitNode.modelMatrix);
+	vec3.transformMat4(hit.normal, hit.normal, canvas.unitNode.modelMatrix);
+
+	return hit;
+} //}}}
+function brushModify(canvas) { //{{{
 	//This function takes in the canvas and x/y coordinates, performing a raycast against the mesh, and modifies the mesh using a the canvas.brush.strength and canvas.brush.falloff properties.
 	//Currently the brush extends to the raycasted element and its immediate neighbors.
@@ -314,10 +461,10 @@
 	if (!canvas.unitNode || canvas.brush.enabled != 'on') { return; }
 	
-	var hit = raycast(canvas, x, y);
+	var hit = canvas.brush.hit;
 
 	if (hit) {
-		var bufferVertices = canvas.unitNode.mesh.getBuffer("vertices");
+		var bufferVertices = canvas.unitNode.mesh.getBuffer('vertices');
 		var vertices = bufferVertices.data;
-		var bufferCoords = canvas.unitNode.mesh.getBuffer("coords");
+		var bufferCoords = canvas.unitNode.mesh.getBuffer('coords');
 		var coords = bufferCoords.data;
 
@@ -361,68 +508,88 @@
 		bufferCoords.upload(canvas.gl.DYNAMIC_DRAW);
 		canvas.unitNode.mesh.octree = new GL.Octree(canvas.unitNode.mesh);	
-	}
-}
-function initializeMarker(canvas, x, y, reset, origin, far) { //{{{
-	//Can be called by onTap to create/reuse a marker, or by the marker's update function. Origin and far are optional and only used by the update function for recreating the raycast.
-	if (!canvas.unitNode) { return; }
-
-	var hit = raycast(canvas, x, y);
-
-	if (hit) {
-		canvas.lastHit = hit;
-		var dataMarkerSize = canvas.dataMarkerSize;
-		if (!canvas.marker) {
-			$('#' + canvas.id).after( '<img src=' + canvas.dataMarkerImage + ' alt="data marker" width="' + dataMarkerSize[0] + '" height="' + dataMarkerSize[1] + '" id="sim-data-marker-' + canvas.id + '" class="sim-data-marker noselect tooltip" data-tooltip-content="#tooltip-content-data-marker-' + canvas.id + '"></img><span id="tooltip-content-data-marker-' + canvas.id + '"></span>');
-			$('#sim-data-marker-' + canvas.id).css({
-				'position': 'absolute', 
-				'left': (Math.round(x) - dataMarkerSize[0] / 2) + 'px', 
-				'top': (Math.round(y) - dataMarkerSize[1]) + 'px', 
-				'width': dataMarkerSize[0] + 'px', 
-				'height': dataMarkerSize[1] + 'px',
-				'pointer-events': 'all',
-				'cursor': 'pointer',
-				'display': 'none'
-			});
-			$('#sim-data-marker-' + canvas.id).tooltipster({
-				contentAsHTML: 'true',
-				animation: 'grow',
-				maxWidth: 320,
-				maxHeight: 320,
-				zIndex: 1000,
-				trigger: 'custom',
-				triggerOpen: {
-					mouseenter: false,
-					originClick: true,
-					touchstart: false
-				},
-				triggerClose: {
-					mouseleave: false,
-					originClick: true,
-					touchleave: false
-				},
-			});
-			canvas.marker = $('#sim-data-marker-' + canvas.id);
-			canvas.marker.on('click touch', function () {
-				canvas.marker.fadeOut(175);
-				canvas.dataMarkerDisplay.tooltipster('close');
-			});
-			canvas.marker.fadeIn(175);
-		}
-		
-		canvas.marker.hit = hit;
-
-		if (!canvas.dataMarkerDisplay) {
-			canvas.dataMarkerDisplay = $('#sim-data-marker-' + canvas.id);
-			canvas.dataMarkerDisplay.tooltipster('open');
-		}
-
-		updatePlot(true);
-		repositionMarker();
-		if (reset) { modifyDataMarkersEnabled(true,canvas); }
-	}
-} //}}}
-function updatePlot(reset) {
-	if (!canvas.lastHit) { return; }
-	var hit = canvas.lastHit;
+		
+		//Update clouds if rendered
+		//TODO: Steven, once you update the cloud generation in applyoptions.js, modify this code block to move the clouds as well. We'll want to move them individually later, but moving them all is ok for now.
+		for (var i = 0; i < canvas.clouds.quantity; i++) {
+			if (canvas.nodes['clouds' + i]) {
+				var v1 = vec3.fromValues(vertices[hit.indices[0] * 3], vertices[hit.indices[0] * 3 + 1], vertices[hit.indices[0] * 3 + 2]);
+				var v2 = vec3.fromValues(vertices[hit.indices[1] * 3], vertices[hit.indices[1] * 3 + 1], vertices[hit.indices[1] * 3 + 2]);
+				var v3 = vec3.fromValues(vertices[hit.indices[2] * 3], vertices[hit.indices[2] * 3 + 1], vertices[hit.indices[2] * 3 + 2]);
+				vec3.transformMat4(v1, v1, canvas.unitNode.modelMatrix);
+				vec3.transformMat4(v2, v2, canvas.unitNode.modelMatrix);
+				vec3.transformMat4(v3, v3, canvas.unitNode.modelMatrix);
+				var x  = (v1[0] + v2[0] + v3[0]) / 3 + Math.floor((Math.random() * (1 + 10000 - (-10000)) + (-10000)));
+				var y  = (v1[1] + v2[1] + v3[1]) / 3 + Math.floor((Math.random() * (1 + 10000 - (-10000)) + (-10000)));
+				var z  = (v1[2] + v2[2] + v3[2]) / 3;
+				canvas.nodes['clouds' + i].translation = [x, y + canvas.clouds.height, z];
+				updateModelMatrix(canvas.nodes['clouds' + i]);
+			}
+		}
+	}
+} //}}}
+function initializeMarker(canvas) { //{{{
+	//Initialize data marker and tooltip display once per page load
+	var marker = $('#' + canvas.dataMarkers.marker.id);
+	var size = canvas.dataMarkers.size;
+	if (!marker.hasClass('tooltipstered')) {
+		marker.css({
+			'position': 'absolute',
+			'left': -size[0] + 'px',
+			'top': -size[1] + '0px',
+			'width': size[0] + 'px',
+			'height': size[1] + 'px',
+			'pointer-events': 'all',
+			'cursor': 'pointer',
+			'display': 'none'
+		});
+		marker.tooltipster({
+			contentAsHTML: true,
+			maxWidth: 320,
+			maxHeight: 320,
+			zIndex: 1000,
+			trigger: 'custom',
+			triggerOpen: {
+				mouseenter: false,
+				originClick: true,
+				touchstart: false
+			},
+			triggerClose: {
+				mouseleave: false,
+				originClick: true,
+				touchleave: false
+			},
+		});
+		marker.on('click touch', function () {
+			marker.fadeOut(175);
+			marker.tooltipster('close');
+		});
+		canvas.dataMarkers.marker.selector = marker;
+	}
+	updateMarker(canvas, true);
+} //}}}
+function repositionMarker(canvas) { //{{{
+	//Mover marker to point to mouse position, offset in y by 1 to enable immediate clicking.
+	if (isEmptyOrUndefined(canvas.dataMarkers.marker.hit) || !canvas.camera.ready) { return; }
+	var size = canvas.dataMarkers.size;
+	var screenPoint = vec3.transformMat4(vec3.create(), canvas.dataMarkers.marker.hit.pos, canvas.camera.vpMatrix);
+	//console.log(canvas, canvas.selector, $(canvas.id)
+	var x = (screenPoint[0] + 1.0) * (canvas.width / 2) + canvas.selector.offset().left;
+	var y = (-screenPoint[1] + 1.0) * (canvas.height / 2) + canvas.selector.offset().top;
+	canvas.dataMarkers.marker.selector.css({
+		'left': (Math.round(x) - size[0] / 2) + 'px', 
+		'top': (Math.round(y) - size[1] + 1) + 'px'
+	});
+	
+	if (canvas.dataMarkers.marker.selector.tooltipster('status').state != 'closed') { canvas.dataMarkers.marker.selector.tooltipster('reposition'); }
+} //}}}
+function updateMarker(canvas, reset) { //{{{
+	//Retrieve data value fields and plots them on data marker popup if a hit has been registered.
+	//TODO: Automatically pick up any field of size md.mesh.numberofelements
+	//If no marker has been placed, no update is needed. If canvas is resimulating and unitNode has not been set yet, wait and try again.
+	if (isEmptyOrUndefined(canvas.dataMarkers.marker.hit)) { return; }
+	if (isEmptyOrUndefined(canvas.unitNode)) { setTimeout( function(){ updateMarker(canvas, reset); }, 750); return; }
+	
+	var hit = canvas.dataMarkers.marker.hit;
+	
 	var coords = canvas.unitNode.mesh.vertexBuffers.coords.data;
 	var latitude = md.mesh.lat;
@@ -431,6 +598,6 @@
 	var velocity;
 	if (md.results[0]) {
-		thickness = md.results[canvas.movieFrame].Thickness;
-		velocity = md.results[canvas.movieFrame].Vel;
+		thickness = md.results[canvas.animation.frame].Thickness;
+		velocity = md.results[canvas.animation.frame].Vel;
 	}
 	else {
@@ -439,5 +606,6 @@
 	}
 	
-	var hitCoords = [coords[hit.indices[0]*2], coords[hit.indices[0]*2+1],coords[hit.indices[1]*2], coords[hit.indices[1]*2+1],coords[hit.indices[2]*2], coords[hit.indices[2]*2+1]];
+	//Determine data values at hit position.
+	var hitCoords = [coords[hit.indices[0]*2], coords[hit.indices[0]*2+1], coords[hit.indices[1]*2], coords[hit.indices[1]*2+1], coords[hit.indices[2]*2], coords[hit.indices[2]*2+1]];
 	var hitLatitude = [latitude[hit.indices[0]], latitude[hit.indices[1]], latitude[hit.indices[2]]];
 	var hitLongitude = [longitude[hit.indices[0]], longitude[hit.indices[1]], longitude[hit.indices[2]]];
@@ -451,142 +619,69 @@
 	var valueThickness = hitThickness[0] * hit.uvw[0] + hitThickness[1] * hit.uvw[1] + hitThickness[2] * hit.uvw[2];
 	var valueVelocity = hitVelocity[0] * hit.uvw[0] + hitVelocity[1] * hit.uvw[1] + hitVelocity[2] * hit.uvw[2];	
-	if (canvas.dataMarkerOptions) {
-		var format = [canvas.dataMarkerOptions.format[0]];	
-		for (var i = 1; i < canvas.dataMarkerOptions.format.length; i++) {
-			var formatString = canvas.dataMarkerOptions.format[i];
-			if (formatString.toLowerCase() == 'x') { format.push(hit.modelPos[0]); }
-			else if (formatString.toLowerCase() == 'y') { format.push(hit.modelPos[1]); }
-			else if (formatString.toLowerCase() == 'z') { format.push(hit.modelPos[2]); }
-			else if (formatString.toLowerCase() == 'lat') { format.push(valueLatitude); }
-			else if (formatString.toLowerCase() == 'long') { format.push(valueLongitude); }
-			else if (formatString.toLowerCase() == 'thickness') { format.push(valueThickness); }
-			else if (formatString.toLowerCase() == 'vel') { format.push(valueVelocity); }
-			else if (formatString.toLowerCase() == 'value') { format.push(value); }
-			else {format.push(formatString); }
-		}
-		if (canvas.dataMarkerOptions.animated) {
-			var isEmpty = (canvas.dataArray.length == 0);
-			var lastUpdatedIndex = (canvas.dataArray.length-1);
-			var newMovieFrame = (!isEmpty && canvas.dataArray[lastUpdatedIndex][0] != canvas.movieFrame);
-			if (reset) {
-				canvas.dataArray = [];
-				newMovieFrame = true;
-				for (var currentFrame = 0; currentFrame < (canvas.unitNode.movieLength); currentFrame++) {
-					coords = canvas.unitNode.texcoords[currentFrame];
-					var hitCoords = [coords[hit.indices[0]*2], coords[hit.indices[0]*2+1],coords[hit.indices[1]*2], coords[hit.indices[1]*2+1],coords[hit.indices[2]*2], coords[hit.indices[2]*2+1]];
-					var u = hitCoords[0] * hit.uvw[0] + hitCoords[2] * hit.uvw[1] + hitCoords[4] * hit.uvw[2];
-					var v = hitCoords[1] * hit.uvw[0] + hitCoords[3] * hit.uvw[1] + hitCoords[5] * hit.uvw[2];
-					var value = canvas.unitNode.caxis[0] * (1.0 - v) + canvas.unitNode.caxis[1] * v;
-					canvas.dataArray.push([currentFrame, value]);
-				}
+
+	//Construct new argument array of the data display format for sprintf using first first argument as the formatSpecifier string and the rest as the additional arguments.
+	var format = canvas.dataMarkers.format.slice();
+	for (var i = 1; i < format.length; i++) {
+		if (format[i].toLowerCase() == 'x') { format[i] = hit.modelPos[0]; }
+		else if (format[i].toLowerCase() == 'y') { format[i] = hit.modelPos[1]; }
+		else if (format[i].toLowerCase() == 'z') { format[i] = hit.modelPos[2]; }
+		else if (format[i].toLowerCase() == 'lat') { format[i] = valueLatitude; }
+		else if (format[i].toLowerCase() == 'long') { format[i] = valueLongitude; }
+		else if (format[i].toLowerCase() == 'thickness') { format[i] = valueThickness; }
+		else if (format[i].toLowerCase() == 'vel') { format[i] = valueVelocity; }
+		else if (format[i].toLowerCase() == 'value') { format[i] = value; }
+	}
+	
+	//Apply changes to tooltip
+	$('#tooltip-content-data-marker-' + canvas.id).html(sprintf.apply(null, format));
+	$('#tooltip-content-data-marker-' + canvas.id).css({'font': canvas.dataMarkers.font});				
+	
+	//If animated, setup animation loop to update plot as movie plays.
+	if (canvas.dataMarkers.animated) {
+		var isEmpty = (canvas.dataMarkers.values.length == 0);
+		var lastUpdatedIndex = (canvas.dataMarkers.values.length-1);
+		var newMovieFrame = (!isEmpty && canvas.dataMarkers.values[lastUpdatedIndex][0] != canvas.animation.frame);
+		//If new data marker has been placed, reinitialize plot. If not, push new value into plot value array.
+		if (reset) {
+			canvas.dataMarkers.values = [];
+			newMovieFrame = true;
+			for (var currentFrame = 0; currentFrame < (canvas.unitNode.movieLength); currentFrame++) {
+				coords = canvas.unitNode.texcoords[currentFrame];
+				var hitCoords = [coords[hit.indices[0]*2], coords[hit.indices[0]*2+1], coords[hit.indices[1]*2], coords[hit.indices[1]*2+1], coords[hit.indices[2]*2], coords[hit.indices[2]*2+1]];
+				var u = hitCoords[0] * hit.uvw[0] + hitCoords[2] * hit.uvw[1] + hitCoords[4] * hit.uvw[2];
+				var v = hitCoords[1] * hit.uvw[0] + hitCoords[3] * hit.uvw[1] + hitCoords[5] * hit.uvw[2];
+				var value = canvas.unitNode.caxis[0] * (1.0 - v) + canvas.unitNode.caxis[1] * v;
+				canvas.dataMarkers.values.push([currentFrame, value]);
 			}
-			else {
-				if (isEmpty || newMovieFrame) {
-					canvas.dataArray.push([canvas.movieFrame,value]);
-				}
+		}
+		else {
+			if (isEmpty || newMovieFrame) {
+				canvas.dataMarkers.values.push([canvas.animation.frame, value]);
 			}
-			if (isEmpty || newMovieFrame) {
-				$('#tooltip-content-data-marker-' + canvas.id).html(sprintf.apply(null, format));
-				$('#tooltip-content-data-marker-' + canvas.id).css({
-					'font': canvas.dataMarkerOptions.font
-				});				
-				var dataLabels = {'latitude':valueLatitude,'longitude':valueLongitude,'thickness':valueThickness,'velocity':valueVelocity,'value':value};
-				var dataDisplay = canvas.dataArray.slice(0,canvas.movieFrame+1);					
-				plot('id','#sim-plot','type','bar','width',400,'height',300,'nticks',25,'xlabel','Time','ylabel',
-					'Value','title','Changes Over Time','datalabels',canvas.dataMarkerOptions.labels,'labelvalues',dataLabels,'data',dataDisplay);
-			}
-		}
-	}
-}
-
-function repositionMarker() {
-	if (!canvas.unitNode) { return; }
-	if (!canvas.dataMarkerDisplay) { return; }
-	var dataMarkerSize = canvas.dataMarkerSize;
-	var screenPoint = vec3.transformMat4(vec3.create(), canvas.marker.hit.pos, canvas.cameraMatrix);
-	var x = screenPoint[0] * (canvas.width / 2) + canvas.width / 2;
-	var y = -screenPoint[1] * (canvas.height / 2) + canvas.height / 2;
-	canvas.marker.css({
-		'left': (Math.round(x) - dataMarkerSize[0] / 2) + 'px', 
-		'top': (Math.round(y) - dataMarkerSize[1]) + 'px'
-	});
-	if (canvas.dataMarkerDisplay.tooltipster('status').state != 'closed') { canvas.dataMarkerDisplay.tooltipster('reposition'); }
-}
-
-function onPan(ev,canvas,displaylog) { //{{{
-	ev.preventDefault();
-	repositionMarker();
-	brushModify(canvas, ev.srcEvent.layerX, ev.srcEvent.layerY);
-	if (ev.type == 'panstart') {
-		canvas.lastDeltaX = 0;
-		canvas.lastDeltaY = 0;
-	}
-	if (ev.srcEvent.shiftKey || ev.pointers.length == 2) {
-		if (!canvas.viewPanning) return;
-		var deltaX = (canvas.lastDeltaX - ev.deltaX) / canvas.clientWidth / canvas.zoom * 2 * canvas.controlSensitivity * 6.371e6;
-		var deltaY = (canvas.lastDeltaY - ev.deltaY) / canvas.clientHeight / canvas.zoom * 2 * canvas.controlSensitivity * 6.371e6;
-		
-		if (canvas.twod) {
-			canvas.translation[0] += Math.cos(DEG2RAD * canvas.rotation[0]) * deltaX - Math.sin(DEG2RAD * 0) * deltaY;
-			canvas.translation[2] += Math.sin(DEG2RAD * canvas.rotation[0]) * deltaX + Math.cos(DEG2RAD * 0) * deltaY;
-		}
-		else {
-			canvas.translation[0] += Math.cos(DEG2RAD * canvas.rotation[0]) * deltaX - Math.sin(DEG2RAD * canvas.rotation[0]) * deltaY;
-			canvas.translation[2] += Math.sin(DEG2RAD * canvas.rotation[0]) * deltaX + Math.cos(DEG2RAD * canvas.rotation[0]) * deltaY;
-		}
-	}
-	else {
-		canvas.rotation[0] += (canvas.lastDeltaX - ev.deltaX) / canvas.clientWidth * -2 * canvas.controlSensitivity * RAD2DEG;
-		canvas.rotation[1] += (canvas.lastDeltaY - ev.deltaY) / canvas.clientHeight * -2 * canvas.controlSensitivity * RAD2DEG;
-		
-		if (canvas.rotation[0] > 360) { canvas.rotation[0] -= 360; };
-		if (canvas.rotation[0] < -360) { canvas.rotation[0] += 360; };
-		if (canvas.rotation[1] > 180) { canvas.rotation[1] -= 360; };
-		if (canvas.rotation[1] < -180) { canvas.rotation[1] += 360; };
-		
-		canvas.rotation[0] = clamp(canvas.rotation[0], canvas.rotationAzimuthBounds[0], canvas.rotationAzimuthBounds[1]);
-		canvas.rotation[1] = clamp(canvas.rotation[1], canvas.rotationElevationBounds[0], canvas.rotationElevationBounds[1])
-	}
-	canvas.lastDeltaX = ev.deltaX;
-	canvas.lastDeltaY = ev.deltaY;
-
-	if (displaylog) { console.log(canvas.rotation); }
-} //}}}
-function onPinch(ev,canvas,displaylog) { //{{{
-	ev.preventDefault();
-	repositionMarker();
-	if (ev.type == 'pinchstart') { canvas.zoomLast = canvas.zoom; }
-	else { modifyZoom(ev.scale * canvas.zoomLast, canvas, displaylog); }
-} //}}}
-function onZoom(ev,canvas,displaylog) { //{{{
-	ev.preventDefault();
-	repositionMarker();
-	var delta = clamp(ev.scale || ev.wheelDelta || -ev.detail, -1, 1) * canvas.controlSensitivity * canvas.zoom / 20;
-	modifyZoom(canvas.zoom + delta, canvas, displaylog);
-} //}}}
-function modifyZoom(value,canvas,displaylog) { //{{{
-	canvas.zoom = clamp(value, canvas.zoomBounds[0], canvas.zoomBounds[1]);
-	if (displaylog) { console.log(canvas.zoom); }
-} //}}}
-function modifyDataMarkersEnabled(value,canvas) { //{{{
-	canvas.dataMarkersEnabled = value;
-	if (!canvas.dataMarkersEnabled && canvas.marker) {
-		canvas.marker.fadeOut(175);
-		canvas.dataMarkerDisplay.tooltipster('close');
-	}
-	else if (canvas.dataMarkersEnabled && canvas.marker) {
-		canvas.marker.fadeIn(175);
-		canvas.dataMarkerDisplay.tooltipster('open');
-	}
-} //}}}
-function toggleMoviePlay(canvas) { //{{{
-	canvas.moviePlay = !canvas.moviePlay;
-	if (canvas.moviePlay){
-		canvas.playButton.find("span").removeClass("fa-play");
-		canvas.playButton.find("span").addClass("fa-pause");
-	}
-	else{
-		canvas.playButton.find("span").removeClass("fa-pause");
-		canvas.playButton.find("span").addClass("fa-play");
+		}
+		
+		//Replot data marker popup using update data value array.
+		if (isEmpty || newMovieFrame) {
+			var dataLabels = {'latitude': valueLatitude, 'longitude': valueLongitude, 'thickness': valueThickness, 'velocity': valueVelocity, 'value': value};
+			var dataDisplay = canvas.dataMarkers.values.slice(0, canvas.animation.frame+1);					
+			plot(
+				'id', '#sim-plot', 
+				'type', 'bar', 
+				'width', 400, 
+				'height', 300, 
+				'nticks', 25, 
+				'xlabel', 'Time', 
+				'ylabel', 'Value', 
+				'title', 'Changes Over Time', 
+				'datalabels', canvas.dataMarkers.labels,
+				'labelvalues', dataLabels,
+				'data', dataDisplay
+			);
+		}
+	}
+	repositionMarker(canvas);
+	if (reset) {
+		canvas.dataMarkers.marker.selector.fadeIn(175);
+		canvas.dataMarkers.marker.selector.tooltipster('open');
 	}
 } //}}}
@@ -594,5 +689,5 @@
 //{{{ Drawing Functions
 function updateCameraMatrix(canvas) { //{{{
-    //Update view matrix and multiply with projection matrix to get the view-projection (camera) matrix.
+    //Update view matrix and multiply with projection matrix to get the view-projection matrix.
 	var vMatrix = mat4.create();
 	var pMatrix = mat4.create();
@@ -604,13 +699,12 @@
 	var cameraPosition = vec3.create();
 
-	if (canvas.twod) { mat4.ortho(pMatrix, -aspectRatio*6.371e6/canvas.zoom, aspectRatio*6.371e6/canvas.zoom, -6.371e6/canvas.zoom, 6.371e6/canvas.zoom, -1.0, 1e10); }
-	else { mat4.perspective(pMatrix, 45 * DEG2RAD, aspectRatio, 1e3, 1e10); }
+	if (canvas.view.twod) { mat4.ortho(pMatrix, -aspectRatio*6.371e6/canvas.view.zoom, aspectRatio*6.371e6/canvas.view.zoom, -6.371e6/canvas.view.zoom, 6.371e6/canvas.view.zoom, canvas.camera.near, canvas.camera.far); }
+	else { mat4.perspective(pMatrix, canvas.camera.fov * DEG2RAD, aspectRatio, canvas.camera.near, canvas.camera.far); }
 	
 	//Apply worldspace translation
-	mat4.translate(translateMatrix, translateMatrix, [-canvas.translation[0],-canvas.translation[1],-canvas.translation[2]]);
-	mat4.multiply(vMatrix, translateMatrix, vMatrix);
+	mat4.translate(vMatrix, translateMatrix, vec3.negate(vec3.create(), canvas.view.position));
 	
 	//Calculate rotation around camera focal point about worldspace origin
-	if (canvas.twod) {
+	if (canvas.view.twod) {
 		mat4.rotate(azimuthRotationMatrix, azimuthRotationMatrix, DEG2RAD * 0, [0, 1, 0]);
 		mat4.rotate(elevationRotationMatrix, elevationRotationMatrix, DEG2RAD * 90, [1, 0, 0]);
@@ -618,26 +712,29 @@
 	}
 	else {
-		mat4.rotate(azimuthRotationMatrix, azimuthRotationMatrix, DEG2RAD * canvas.rotation[0], [0, 1, 0]);
-		mat4.rotate(elevationRotationMatrix, elevationRotationMatrix, DEG2RAD * canvas.rotation[1], [1, 0, 0]);
+		mat4.rotate(azimuthRotationMatrix, azimuthRotationMatrix, DEG2RAD * canvas.view.rotation[0], [0, 1, 0]);
+		mat4.rotate(elevationRotationMatrix, elevationRotationMatrix, DEG2RAD * canvas.view.rotation[1], [1, 0, 0]);
 		mat4.multiply(rotationMatrix, elevationRotationMatrix, azimuthRotationMatrix);
 	}
-	
+
 	//Apply rotation transform
 	mat4.multiply(vMatrix, rotationMatrix, vMatrix);
-
-	//Apply screenspace translation
+	
+	//Apply screenspace translation to emulate rotation around point
 	mat4.identity(translateMatrix);
-	mat4.translate(translateMatrix, translateMatrix, [0.0, 0.0, -6.371e6/canvas.zoom]);
+	mat4.translate(translateMatrix, translateMatrix, [0.0, 0.0, -6.371e6/canvas.view.zoom]);
 	mat4.multiply(vMatrix, translateMatrix, vMatrix);
 	
-	//Calculate fields for lighting and raycasts
-	mat4.invert(canvas.vInverseMatrix, vMatrix);
-	
 	//Apply projection matrix to get camera matrix
-	mat4.multiply(canvas.cameraMatrix, pMatrix, vMatrix);
-	mat4.invert(canvas.inverseCameraMatrix, canvas.cameraMatrix);
-	vec3.transformMat4(canvas.cameraPosition, cameraPosition, canvas.inverseCameraMatrix);
+	mat4.copy(canvas.camera.vMatrix, vMatrix);
+	mat4.multiply(canvas.camera.vpMatrix, pMatrix, vMatrix);
+	
+	//Calculate inverse view matrix fields for lighting and raycasts
+	mat4.invert(canvas.camera.vInverseMatrix, canvas.camera.vMatrix);
+	mat4.invert(canvas.camera.vpInverseMatrix, canvas.camera.vpMatrix);
+	
+	vec3.transformMat4(canvas.camera.position, cameraPosition, canvas.camera.vpInverseMatrix);
+	canvas.camera.ready = true;
 }//}}}
-function drawSceneGraphNode(canvas,node) { //{{{
+function drawSceneGraphNode(canvas, node) { //{{{
 	if (!node.enabled) { return; }
 
@@ -646,5 +743,12 @@
 	
 	var mvpMatrix = mat4.create();
-	mat4.multiply(mvpMatrix, canvas.cameraMatrix, node.modelMatrix);
+	mat4.multiply(mvpMatrix, canvas.camera.vpMatrix, node.modelMatrix);
+	
+	var mvMatrix = mat4.create();
+	mat4.multiply(mvMatrix, canvas.camera.vMatrix, node.modelMatrix);
+	
+	var normalMatrix = mat4.create();
+	mat4.invert(normalMatrix, mvMatrix);
+	mat4.transpose(normalMatrix, normalMatrix);
 	
 	if (node.texture) { node.texture.bind(0); }
@@ -660,5 +764,5 @@
 	var lightOrigin = vec3.fromValues(0, 0, 0);
 	var cameraPositionRelative = vec3.create();
-	vec3.transformMat4(origin, origin, canvas.vInverseMatrix);
+	vec3.transformMat4(origin, origin, canvas.camera.vInverseMatrix);
 	vec3.normalize(lightOrigin, lightOrigin);
 	vec3.sub(cameraPositionRelative, origin, node.translation);
@@ -692,5 +796,9 @@
 	node.shader.uniforms({
 		m4MVP: mvpMatrix,
+		m4Normal: normalMatrix,
 		m4Model: node.modelMatrix,
+		//u_lightPosition: [-lightOrigin[0], -lightOrigin[1], -lightOrigin[2]],
+		u_lightPosition: [1.0, 1.0, 1.0],
+		u_diffuseColor: [1.0, 0.9, 0.9],
 		u_texture: 0,
 		u_alpha: node.alpha,
@@ -728,41 +836,48 @@
 	gl.disable(gl.CULL_FACE);
 } //}}}
-function draw(canvas,options) { //{{{
-	if (canvas.textcanvas) { canvas.textcanvas.draw(canvas); }
-
+function draw(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 (nodes.length < 1) {
-		canvas.drawHandler = window.requestAnimationFrame(function(time) { draw(canvas,options); });
-		return;
-	}
-	for (var node in nodes) {	
-		if ((nodes[node].texture && nodes[node].texture.ready == false) || nodes[node].shader.ready == false || nodes[node].mesh.ready == false) {
-			canvas.drawHandler = window.requestAnimationFrame(function(time) { draw(canvas,options); });
-			return;
-		}
-	}
-	
-	var rect = canvas.getBoundingClientRect();
-	canvas.width  = rect.width;
-	canvas.height = rect.height;
-	
-	var gl = canvas.gl;
-	gl.makeCurrent(); //litegl function to handle switching between multiple canvases
-	gl.viewport(0, 0, rect.width, rect.height);
-	gl.clearColor(canvas.backgroundcolor[0], canvas.backgroundcolor[1], canvas.backgroundcolor[2], canvas.backgroundcolor[3]);
-	gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
-	
-	updateCameraMatrix(canvas);
-	
-	//if (canvas.marker) { canvas.marker.update(); }
-	
-	var drawPassNumber = 3;
-	for (var i = drawPassNumber - 1; i >= 0; i--) {
-		for (var node in nodes) {
-			if (nodes[node].drawOrder == i) { drawSceneGraphNode(canvas,nodes[node]); }
-		}
-	}
-
-	canvas.drawHandler = window.requestAnimationFrame(function(time) { draw(canvas,options); });
+	if (!canvas.draw.ready) {
+		if (nodes.length !== 0) {
+			canvas.draw.ready = true;
+			for (var node in nodes) {
+				if (nodes[node].shader.ready == false) {
+					canvas.draw.ready = false;
+					break;
+				}
+			}
+			
+		}
+	}
+	
+	//Begin rendering nodes
+	if (canvas.draw.ready) {
+		if (canvas.textcanvas) { canvas.textcanvas.draw(canvas); }
+		if (canvas.overlaycanvas) { canvas.overlaycanvas.draw(canvas); }
+	
+		var rect = canvas.getBoundingClientRect();
+		canvas.width  = rect.width;
+		canvas.height = rect.height;
+		
+		var gl = canvas.gl;
+		gl.makeCurrent(); //litegl function to handle switching between multiple canvases
+		gl.viewport(0, 0, canvas.width, canvas.height);
+		gl.clearColor(canvas.backgroundcolor[0], canvas.backgroundcolor[1], canvas.backgroundcolor[2], canvas.backgroundcolor[3]);
+		gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
+		
+		updateCameraMatrix(canvas);
+		
+		var drawPassNumber = 3;
+		for (var i = drawPassNumber - 1; i >= 0; i--) {
+			for (var node in nodes) {
+				if (nodes[node].drawOrder == i) { drawSceneGraphNode(canvas, nodes[node]); }
+			}
+		}
+	}
+	
+	//Regardless of ready state, schedule next frame to check for ready state and render
+	canvas.draw.handler = window.requestAnimationFrame(function(time) { draw(canvas); });
 } //}}}
 //}}}
