Index: issm/trunk-jpl/src/m/plot/applyoptions.js
===================================================================
--- issm/trunk-jpl/src/m/plot/applyoptions.js	(revision 21828)
+++ issm/trunk-jpl/src/m/plot/applyoptions.js	(revision 21911)
@@ -6,5 +6,5 @@
 	//
 	//   See also: PLOTMODEL, PARSE_OPTIONS
-	
+
 	//{{{ colorbar
 	var gl = canvas.gl;
@@ -16,5 +16,5 @@
 			var ccanvasid, ctitleid, clabelsid, ccanvas, ctitle, clabels, ccontext, cmap, colorbar, cwidth, cheight, cgradient, color, y, x;
 			//}}}
-			//{{{ Create colorbar labels 
+			//{{{ Create colorbar labels
 			var labels = [];
 			var cdivisions = options.getfieldvalue('colorbarnticks', 6);
@@ -49,5 +49,5 @@
 			} //}}}
 			//{{{ Initialize colorbar canvas
-			ccanvasid = options.getfieldvalue('colorbarid', options.getfieldvalue('canvasid').replace('canvas','colorbar-canvas'));			
+			ccanvasid = options.getfieldvalue('colorbarid', options.getfieldvalue('canvasid').replace('canvas','colorbar-canvas'));
 			ccanvas = $('#'+ccanvasid)[0];
 			cwidth = ccanvas.width*options.getfieldvalue('colorbarwidth', 1);
@@ -63,5 +63,5 @@
 			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)];	
+				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)');
 			}
@@ -107,5 +107,5 @@
 			if (options.exist('colorbartitle')) { ctitle.html(options.getfieldvalue('colorbartitle')); }
 			//}}}
-		} 
+		}
 	} //}}}
 	//{{{ texture canvas
@@ -119,13 +119,13 @@
 	tcontext = tcanvas.getContext('2d');
 	tgradient = tcontext.createLinearGradient(0, 0, 0, 256);
-		
+
 	var cmap = options.getfieldvalue('colormap','jet');
 	var colorbar = colorbars[cmap];
 	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)];	
+		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);
@@ -134,94 +134,46 @@
 	//}}}
 	//{{{ text display
-	if (options.exist('textlabels')) {
-		var textcanvas, textcanvasid;	
-		textcanvasid = options.getfieldvalue('textcanvasid', options.getfieldvalue('canvasid')+'-text');
-		textcanvas = $('#'+textcanvasid);
-		textcanvas.textlabels = options.getfieldvalue('textlabels',[]);
+	var overlaycanvasid = options.getfieldvalue('overlayid', options.getfieldvalue('canvasid')+'-overlay');
+	var overlaycanvas = $('#'+overlaycanvasid)[0];
+	if (!isEmptyOrUndefined(overlaycanvas)) {
+		//Get drawing context and save reference on main WebGL canvas
+		var ctx = overlaycanvas.getContext('2d');
+		canvas.overlaycanvas = overlaycanvas;
 		
-		//setup drawing function for text canvas draw calls
-		textcanvas.draw = function(canvas) {
-			var textcontext, textlabels, textlabel, textcanvaswidth, textcanvasheight, textcoordinates;	
-			var textposition = vec3.create();
-			var mvpMatrix = mat4.create();
-			
-			//ensure correct canvas coordinate scaling
-			textcanvaswidth = this[0].clientWidth;
-			textcanvasheight = this[0].clientHeight;
-			this[0].width  = textcanvaswidth;
-			this[0].height = textcanvasheight;
-			
-			textcontext = this[0].getContext('2d');
-			textlabels = options.getfieldvalue('textlabels',[]);
-			textcontext.clearRect(0, 0, textcanvaswidth, textcanvasheight);
-			
-			//worldspace to screenspace transformation for text
-			for (text in textlabels) {
-				textlabel = textlabels[text];
-				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
-					continue;
-				}
-				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.fillStyle = options.getfieldvalue('colorbarfontcolor','black');
-				textcontext.strokeStyle = options.getfieldvalue('colorbarfontcolor','black');
-				textcontext.textAlign = 'center';
-				textcontext.textBaseline = 'middle';
-				textcontext.fillText(textlabel.text, textcoordinates[0], textcoordinates[1]);
-				textcontext.strokeText(textlabel.text, textcoordinates[0], textcoordinates[1]);
-			}
-		}
-		canvas.textcanvas = textcanvas;
-	} //}}}
+		//Resize interal viewport coordinates to match screenspace coordinates
+		var rect = overlaycanvas.getBoundingClientRect();
+		overlaycanvas.width  = rect.width;
+		overlaycanvas.height = rect.height;
+		
+		//Clear canvas each frame for any new drawings
+		canvas.overlayHandlers['draw'] = function(overlaycanvas) {
+			ctx.clearRect(0, 0, overlaycanvas.width, overlaycanvas.height);
+		}
+	}
 	//{{{ lat long overlay
 	if (options.exist('latlongoverlay')) {
-		var 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');
+		canvas.overlayHandlers['latlong'] = function(canvas) {
+			//Transform from world space to viewport space
 			var centerx = overlaycanvas.width / 2;
 			var centery = overlaycanvas.height / 2;
 			var radius = (overlaycanvas.height) / 2;
+
+			//Draw latitudes
 			ctx.setLineDash([5, 10]);
 			for(latitude in latitudes) {
@@ -229,5 +181,5 @@
 				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.font = 'bold ' + String(options.getfieldvalue('colorbarfontsize', 18))+'px "Lato", Helvetica, Arial, sans-serif';
 				ctx.fillStyle = options.getfieldvalue('colorbarfontcolor','black');
 				ctx.strokeStyle = options.getfieldvalue('colorbarfontcolor','black');
@@ -237,4 +189,5 @@
 				ctx.strokeText(latitude, centerx, centery + radius * latitudes[latitude]);
 			}
+			//Draw longitudes
 			ctx.setLineDash([1, 0]);
 			for (longitude in longitudes) {
@@ -245,37 +198,83 @@
 			}
 		}
-		canvas.overlaycanvas = overlaycanvas;
+	} //}}}
+	if (options.exist('textlabels')) {
+		//Attatch new overlay handler to display text labels
+		var textLabels = options.getfieldvalue('textlabels',[]);
+		canvas.overlayHandlers['text'] = function(canvas) {
+			for (var i = 0; i < textLabels.length; i++) {
+				//Get text label to display
+				var textLabel = textLabels[i];
+				textLabel = {
+					position: defaultFor(textLabel.position, vec3.create()),
+					text: defaultFor(textLabel.text, ''),
+					fontSize: defaultFor(textLabel.fontSize, 18),
+					fontColor: defaultFor(textLabel.fontColor, 'black'),
+
+				};
+				
+				// function declared in slr-gfm sim-front-end-controller.js
+				// if labels are behind the globe sphere then skip iteartion and do not display them
+				if (isLabelVisible(textLabel)) {
+					//Transform from world space to viewport space
+					var screenPoint = vec3.transformMat4(vec3.create(), textLabel.position, canvas.camera.vpMatrix);
+					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;
+					
+					//Draw text
+					ctx.font = 'bold ' + String(textLabel.fontSize) + 'px Arial Black, sans-serif';
+					ctx.fillStyle = textLabel.fontColor;
+					ctx.strokeStyle = 'white';
+					ctx.textAlign = 'center';
+					ctx.textBaseline = 'middle';
+					ctx.fillText(textLabel.text, x, y);
+					ctx.strokeText(textLabel.text, x, y);
+				}
+			}
+		}
 	} //}}}
 	//{{{ additional rendering nodes
 	if (options.exist('render')) {
 		var meshresults = processmesh(md, data, options);
-		var x = meshresults[0]; 
-		var y = meshresults[1]; 
-		var z = meshresults[2]; 
+		var x = meshresults[0];
+		var y = meshresults[1];
+		var z = meshresults[2];
 		var elements = meshresults[3];
-		var is2d = meshresults[4]; 
+		var is2d = meshresults[4];
 		var isplanet = meshresults[5];
-		
+
 		var xlim = options.getfieldvalue('xlim', [ArrayMin(x), ArrayMax(x)]);
 		var ylim = options.getfieldvalue('ylim', [ArrayMin(y), ArrayMax(y)]);
 		var zlim = options.getfieldvalue('zlim', [ArrayMin(z), ArrayMax(z)]);
 
-		var global = vec3.length([(xlim[0] + xlim[1]) / 2, (ylim[0] + ylim[1]) / 2, (zlim[0] + zlim[1]) / 2]) < 6371000/10; //tolerance for global models = center is 637100 meters away from center of earth	
-		var translation = global ? [(xlim[0] + xlim[1]) / 2, (ylim[0] + ylim[1]) / 2, (zlim[0] + zlim[1]) / 2] : [(xlim[0] + xlim[1]) / 2, (ylim[0] + ylim[1]) - 6371000, (zlim[0] + zlim[1]) / 2];
-		
+		var global = vec3.length([(xlim[0] + xlim[1]) / 2, (ylim[0] + ylim[1]) / 2, (zlim[0] + zlim[1]) / 2]) < 6371000/10; //tolerance for global models = center is 637100 meters away from center of earth
+		var translation = global ? [(xlim[0] + xlim[1]) / 2, (ylim[0] + ylim[1]) / 2, (zlim[0] + zlim[1]) / 2] : canvas.view.position;
+
 		var renderObjects = options.getfieldvalue('render',{});
-		if ('sky' in renderObjects && !('sky' in canvas.nodes)) {
-			var object = renderObjects.sky;
+		for (var renderObject in renderObjects) {
+			//Modify renderObejct?
+			var object = renderObjects[renderObject];
 			object = {
-				enabled: defaultFor(object.enabled, true),
-				scale: defaultFor(object.scale, 1),
+				enabled: defaultFor(object.enabled, true),					//Toggle display of the render object node
+				scale: defaultFor(object.scale, 1),							//Model matrix scaling
+				x: defaultFor(object.x, [0.0, 1.0, 0.0, 0.0, 0.0, 0.0]),	//x coordinate array
+				y: defaultFor(object.y, [0.0, 0.0, 0.0, 1.0, 0.0, 0.0]),	//y coordinate array
+				z: defaultFor(object.z, [0.0, 0.0, 0.0, 0.0, 0.0, 1.0]),	//z coordinate array
+				indices: defaultFor(object.indices, []),					//indices array
+				name: defaultFor(object.name, 'NY'),						//Text to display for cities.
+				size: defaultFor(object.size, 1),							//Physical size of the object in meters
+				color: defaultFor(object.color, 'black'),					//Diffuse color of object
+				height: defaultFor(object.height, 25000),					//Height of object along y axis, currently for clouds only
+				range: defaultFor(object.range, 120000),						//Range of sz plane to spawn object, currently for clouds only
+				quantity: defaultFor(object.quantity, 15)					//Quantity of objects to display, currently for clouds only
 			};
-			if (object.enabled) {
-				var atmosphereScale = 1.25;
-				var mesh = GL.Mesh.icosahedron({size: 6371000 * atmosphereScale, subdivisions: 5});
+			if (!object.enabled) { continue; }
+			if ('sky' === renderObject && !('sky' in canvas.nodes)) {
+				var mesh = GL.Mesh.icosahedron({size: 6371000 * canvas.atmosphere.scaleHeight, subdivisions: 5});
 				var texture = initTexture(gl, canvas.rootPath + 'textures/TychoSkymapII_t4_2k.jpg');
 				node = new Node(
 					'canvas', canvas,
 					'options', options,
+					'renderObject', object,
 					'name', 'sky',
 					'shaderName', 'SkyFromSpace',
@@ -286,12 +285,5 @@
 				);
 			}
-		}
-		if ('space' in renderObjects && !('space' in canvas.nodes)) {
-			var object = renderObjects.space;
-			object = {
-				enabled: defaultFor(object.enabled, true),
-				scale: defaultFor(object.scale, 1),
-			};
-			if (object.enabled) {
+			if ('space' === renderObject && !('space' in canvas.nodes)) {
 				var mesh = GL.Mesh.sphere({size: 6371000 * 20});
 				var texture = initTexture(gl, canvas.rootPath + 'textures/TychoSkymapII_t4_2k.jpg');
@@ -299,4 +291,5 @@
 					'canvas', canvas,
 					'options', options,
+					'renderObject', object,
 					'name', 'space',
 					'shaderName', 'Textured',
@@ -308,18 +301,9 @@
 				);
 			}
-		}
-		if ('coastlines' in renderObjects && !('coastlines' in canvas.nodes)) {
-			var object = renderObjects.coastlines;
-			object = {
-				enabled: defaultFor(object.enabled, false),
-				scale: defaultFor(object.scale, 1),
-				x: defaultFor(object.x, {}),
-				y: defaultFor(object.y, {}),
-				z: defaultFor(object.z, {})
-			};
-			if (object.enabled) {
-				node = new Node(
-					'canvas', canvas,
-					'options', options,
+			if ('coastlines' === renderObject && !('coastlines' in canvas.nodes)) {
+				node = new Node(
+					'canvas', canvas,
+					'options', options,
+					'renderObject', object,
 					'name', 'coastlines',
 					'shaderName', 'Colored',
@@ -331,18 +315,9 @@
 				node.patch('Vertices', [object.x, object.y, object.z], 'FaceColor', 'none');
 			}
-		}
-		if ('graticule' in renderObjects && !('graticule' in canvas.nodes)) {
-			var object = renderObjects.graticule;
-			object = {
-				enabled: defaultFor(object.enabled, false),
-				scale: defaultFor(object.scale, 1),
-				x: defaultFor(object.x, {}),
-				y: defaultFor(object.y, {}),
-				z: defaultFor(object.z, {})
-			};
-			if (object.enabled) {			
-				node = new Node(
-					'canvas', canvas,
-					'options', options,
+			if ('graticule' === renderObject && !('graticule' in canvas.nodes)) {
+				node = new Node(
+					'canvas', canvas,
+					'options', options,
+					'renderObject', object,
 					'name', 'graticule',
 					'shaderName', 'Colored',
@@ -350,28 +325,16 @@
 					'lineWidth', options.getfieldvalue('linewidth', 1),
 					'scale', [object.scale, object.scale, object.scale],
-					'rotation', [-90, 0, 0]
+					'rotation', [-90, 180, 0]
 				);
 				node.patch('Vertices', [object.x, object.y, object.z], 'FaceColor', 'none');
 			}
-		}
-		if ('cities' in renderObjects && !('cities' in canvas.nodes)) {
-			var object = renderObjects.cities;
-			object = {
-				enabled: defaultFor(object.enabled, false),
-				scale: defaultFor(object.scale, 1),
-				size: defaultFor(object.size, 1.5),
-				color: defaultFor(object.color, 'black'),
-				x: defaultFor(object.x, {}),
-				y: defaultFor(object.y, {}),
-				z: defaultFor(object.z, {}),
-				indices: defaultFor(object.indices, {})
-			};
-			if (object.enabled) {
+			if ('cities' === renderObject && !('cities' in canvas.nodes)) {
 				var mesh = GL.Mesh.icosahedron({size: object.size, subdivisions: 1});
 				node = new Node(
 					'canvas', canvas,
 					'options', options,
+					'renderObject', object,
 					'name', 'cities',
-					'shaderName', 'Colored',
+					'shaderName', 'ColoredDiffuse',
 					'diffuseColor', object.color,
 					'lineWidth', options.getfieldvalue('linewidth', 1),
@@ -381,17 +344,19 @@
 				node.geometryShader('Mesh', mesh, 'Vertices', [object.x, object.y, object.z], 'Indices', object.indices);
 			}
-		}
-		if ('city' in renderObjects) {
-			var object = renderObjects.city;
-			object = {
-				enabled: defaultFor(object.enabled, false),
-				name: defaultFor(object.name, 'NY'),
-				size: defaultFor(object.size, 15000),
-				color: defaultFor(object.color, 'magenta'),
-				x: defaultFor(object.x, {}),
-				y: defaultFor(object.y, {}),
-				z: defaultFor(object.z, {})
-			};
-			if (object.enabled) {
+			if ('axis' === renderObject && !('axis' in canvas.nodes)) {		
+				node = new Node(
+					'canvas', canvas,
+					'options', options,
+					'renderObject', object,
+					'name', 'axis',
+					'shaderName', 'Colored',
+					'drawMode', gl.LINES,
+					'lineWidth', options.getfieldvalue('linewidth', 1),
+					'scale', [object.scale, object.scale, object.scale],
+					'rotation', [0, 0, 0]
+				);
+				node.patch('Vertices', [object.x, object.y, object.z], 'FaceColor', 'none');
+			}
+			if ('city' === renderObject) {
 				//city
 				var mesh = GL.Mesh.sphere({size: object.size});
@@ -399,26 +364,20 @@
 					'canvas', canvas,
 					'options', options,
+					'renderObject', object,
 					'name', 'city',
-					'shaderName', 'Colored',
+					'shaderName', 'ColoredDiffuse',
 					'diffuseColor', object.color,
 					'mesh', mesh,
 					'translation', [object.x, object.z, -object.y]
 				);
-				
-				var overlaycanvasid = options.getfieldvalue('overlayid', options.getfieldvalue('canvasid')+'-overlay');
-				var overlaycanvas = $('#'+overlaycanvasid)[0];
-				overlaycanvas.draw = function(canvas) {
-					var rect = overlaycanvas.getBoundingClientRect();
-					overlaycanvas.width  = rect.width;
-					overlaycanvas.height = rect.height;
-					var ctx = overlaycanvas.getContext('2d');
+				//Attatch new overlay handler to display city name
+				canvas.overlayHandlers['city'] = function(canvas) {
 					var node = canvas.nodes['city'];
-					node.translation = [object.x, object.z, -object.y];
-					node.updateModelMatrix();
+					var object = node.renderObject;
 					var screenPoint = vec3.transformMat4(vec3.create(), node.translation, canvas.camera.vpMatrix);
 					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;
 
-					ctx.font = String(options.getfieldvalue('colorbarfontsize', 22))+'px Arial Black, sans-serif';
+					ctx.font = 'bold ' + String(options.getfieldvalue('colorbarfontsize', 22))+'px Arial Black, sans-serif';
 					ctx.fillStyle = options.getfieldvalue('colorbarfontcolor','black');
 					ctx.strokeStyle = 'white';
@@ -428,23 +387,27 @@
 					ctx.strokeText(object.name, x, y);
 				}
-				canvas.overlaycanvas = overlaycanvas;
-			}
-		}
-		if (canvas.clouds.enabled) {
-			//clouds
-			for (var i = 0; i < canvas.clouds.quantity; i++) {
+			}
+			if ('clouds' === renderObject && !('clouds0' in canvas.nodes)) {
+				//clouds				
 				var mesh = GL.Mesh.fromURL(canvas.rootPath+'obj/cloud.obj');
-				translation = [translation[0] + Math.floor((Math.random() * (1 + 12000 - (-12000)) + (-12000))), translation[1] - 405000, translation[2]  + Math.floor((Math.random() * (1 + 12000 - (-12000)) + (-12000)))];
-				node = new Node(
-					'canvas', canvas,
-					'options', options,
-					'name', 'clouds' + i,
-					'shaderName', 'Textured',
-					'animation', {'time': Date.now(),'target': translation,'current': translation},
-					'diffuseColor', [0.7,0.7,0.7,1.0],
-					'mesh', mesh,
-					'scale', [2500, 2500, 2500],
-					'translation', translation
-				);
+				for (var i = 0; i < object.quantity; i++) {
+					//TODO: More options, less magic numbers. Add animation. Better shading.
+					var offset = [	translation[0] + (Math.random() - 0.5) * 2 * object.range, 
+									translation[1] + object.height + (Math.random() - 0.5) * 0.2 * object.range, 
+									translation[2] + (Math.random() - 0.5) * 2 * object.range];
+					node = new Node(
+						'canvas', canvas,
+						'options', options,
+						'renderObject', object,
+						'name', 'clouds' + i,
+						'shaderName', 'ColoredDiffuse',
+						'animation', {'time': Date.now(),'target': translation,'current': translation},
+						'diffuseColor', [0.7,0.7,0.7,1.0],
+						'specularColor', [0.0,0.0,0.0,1.0],
+						'mesh', mesh,
+						'scale', [object.scale, object.scale, object.scale],
+						'translation', offset
+					);
+				}
 			}
 		}
Index: issm/trunk-jpl/src/m/plot/plot_mesh.js
===================================================================
--- issm/trunk-jpl/src/m/plot/plot_mesh.js	(revision 21828)
+++ issm/trunk-jpl/src/m/plot/plot_mesh.js	(revision 21911)
@@ -31,5 +31,5 @@
 	}
 	else {
-		vertices = Node.prototype.scaleVertices(md, x, y, z, options.getfieldvalue('heightscale', 1));
+		vertices = Node.prototype.scaleVertices(md, x, y, z, elements, options.getfieldvalue('heightscale', 1), options.getfieldvalue('maskscale',{}));
 		scale = [1, 1, 1];
 	}
Index: issm/trunk-jpl/src/m/plot/plot_overlay.js
===================================================================
--- issm/trunk-jpl/src/m/plot/plot_overlay.js	(revision 21828)
+++ issm/trunk-jpl/src/m/plot/plot_overlay.js	(revision 21911)
@@ -41,5 +41,5 @@
 	}
 	else {
-		vertices = Node.prototype.scaleVertices(md, x, y, z, options.getfieldvalue('heightscale', 1));
+		vertices = Node.prototype.scaleVertices(md, x, y, z, elements, options.getfieldvalue('heightscale', 1), options.getfieldvalue('maskscale',{}));
 		scale = [1, 1, 1];
 	}
@@ -52,9 +52,11 @@
 		'options', options,
 		'name', 'overlay',
-		'shaderName', 'ground' in options.getfieldvalue('render', {}) ? 'GroundFromSpace' : 'Textured',
+		'shaderName', 'ground' in options.getfieldvalue('render', {}) ? 'GroundFromSpace' : 'TexturedDiffuse',
 		'alpha', options.getfieldvalue('outeralpha', 1.0),
 		//'center', [(xlim[0] + xlim[1]) / 2, (ylim[0] + ylim[1]) / 2, md.mesh.classname() === 'mesh3dsurface' ? (zlim[0] + zlim[1]) / 2 : zlim[0]],
 		'center', [(xlim[0] + xlim[1]) / 2, (ylim[0] + ylim[1]) / 2, (zlim[0] + zlim[1]) / 2],
-		'diffuseColor', 'white',
+		'ambientColor', [0.1, 0.1, 0.1 ,1.0],
+		'diffuseColor', [1.0, 1.0, 1.0 ,1.0],
+		'specularStrength', 0.0,
 		'maskEnabled', options.getfieldvalue('outermask','off') == 'on',
 		'maskHeight', options.getfieldvalue('outermaskheight', 150.0),
@@ -65,10 +67,10 @@
 	);
 	//}}}
-	
+
 	var xRange = xlim[1] - xlim[0];
 	var yRange = ylim[1] - ylim[0];
 	var coordArray = [new Array(x.length), new Array(x.length)];
+	
 	//generate mesh:
-	
 	if (md.mesh.classname() == 'mesh3dsurface') {
 		var xyz, magnitude;
@@ -87,4 +89,4 @@
 		}
 	}
-	node.patch('Faces', elements, 'Vertices', vertices, 'FaceVertexCData', coordArray, 'FaceColor', 'interp', 'EdgeColor', edgecolor);
+	node.patch('Faces', elements, 'Vertices', vertices, 'FaceVertexCData', coordArray, 'FaceColor', 'interp');
 } //}}}
Index: issm/trunk-jpl/src/m/plot/plot_quiver.js
===================================================================
--- issm/trunk-jpl/src/m/plot/plot_quiver.js	(revision 21828)
+++ issm/trunk-jpl/src/m/plot/plot_quiver.js	(revision 21911)
@@ -37,5 +37,5 @@
 	}
 	else {
-		vertices = Node.prototype.scaleVertices(md, x, y, z, options.getfieldvalue('heightscale', 1));
+		vertices = Node.prototype.scaleVertices(md, x, y, z, elements, options.getfieldvalue('heightscale', 1), options.getfieldvalue('maskscale',{}));
 		scale = [1, 1, 1];
 	}
Index: issm/trunk-jpl/src/m/plot/plot_unit.js
===================================================================
--- issm/trunk-jpl/src/m/plot/plot_unit.js	(revision 21828)
+++ issm/trunk-jpl/src/m/plot/plot_unit.js	(revision 21911)
@@ -13,5 +13,5 @@
 	//}
 	//else {
-		
+
 	//{{{ declare variables:
 	//Process data and model
@@ -37,10 +37,10 @@
 	}
 	else {
-		vertices = Node.prototype.scaleVertices(md, x, y, z, options.getfieldvalue('heightscale', 1));
+		vertices = Node.prototype.scaleVertices(md, x, y, z, elements, options.getfieldvalue('heightscale', 1), options.getfieldvalue('maskscale',{}));
 		scale = [1, 1, 1];
 	}
 	
 	//Compute gl variables:
-	var edgecolor = options.getfieldvalue('edgecolor', 'black');
+	var edgecolor = options.getfieldvalue('edgecolor', [1.0, 1.0, 1.0 ,1.0]);
 	var maskzeros = options.getfieldvalue('maskzeros', {});
 	var node = new Node(
@@ -48,10 +48,12 @@
 		'options', options,
 		'name', 'unit',
-		'shaderName', 'Textured',
+		'shaderName', 'TexturedDiffuse',
 		'alpha', options.getfieldvalue('alpha', 1.0),
 		'caxis', options.getfieldvalue('caxis',[ArrayMin(data), ArrayMax(data)]),
 		//'center', [(xlim[0] + xlim[1]) / 2, (ylim[0] + ylim[1]) / 2, md.mesh.classname() === 'mesh3dsurface' ? (zlim[0] + zlim[1]) / 2 : zlim[0]],
 		'center', [(xlim[0] + xlim[1]) / 2, (ylim[0] + ylim[1]) / 2, (zlim[0] + zlim[1]) / 2],
+		'lightingBias', 0.8,
 		'diffuseColor', edgecolor,
+		'specularStrength', 0.0,
 		'enabled', options.getfieldvalue('nodata','off') == 'off',
 		'log', options.getfieldvalue('log',false),
Index: issm/trunk-jpl/src/m/plot/webgl.js
===================================================================
--- issm/trunk-jpl/src/m/plot/webgl.js	(revision 21828)
+++ issm/trunk-jpl/src/m/plot/webgl.js	(revision 21911)
@@ -9,7 +9,9 @@
 		if (!isEmptyOrUndefined(canvas.animation) && canvas.animation.handler !== 0) { clearInterval(canvas.animation.handler); }
 		initWebGL(canvas, options);
-		initializeMarker(canvas);
 		draw(canvas);
 		canvas.initialized = true;
+		
+		//The onStart event triggers once per plotmodel call load after WebGL and canvas initialization are complete
+		canvas.selector.trigger('onStart', [canvas]);
 	}
 	return canvas;
@@ -34,7 +36,7 @@
 		mc.add(new Hammer.Pan({threshold: 0, pointers: 0}));
 		mc.add(new Hammer.Pinch({threshold: 0})).recognizeWith(mc.get('pan'));
-		mc.on('tap press', function (ev) {onTap(ev, canvas);});
+		mc.on('tap', function (ev) {onTap(ev, canvas);});
 		mc.on('panstart panmove', function (ev) {onPan(ev, canvas, displayview);});
-		mc.on('pinchstart pinchmove', function (ev) {onPinch(ev, canvas, displayview);});		
+		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);
@@ -46,4 +48,5 @@
 		canvas.unitData = {};
 		canvas.unitMovieData = {};
+		
 		canvas.gl = gl;
 		canvas.rootPath = options.getfieldvalue('rootpath', '../../../js/');
@@ -57,5 +60,7 @@
 	
 	//Add context state variables
+	canvas.render = options.getfieldvalue('render', {});
 	canvas.controlSensitivity = options.getfieldvalue('controlsensitivity', 1);
+	canvas.overlayHandlers = {};
 	var backgroundcolor = new RGBColor(options.getfieldvalue('backgroundcolor', 'lightcyan'));
 	if (backgroundcolor.ok) { canvas.backgroundcolor = [backgroundcolor.r/255.0, backgroundcolor.g/255.0, backgroundcolor.b/255.0, 1.0]; }
@@ -63,72 +68,88 @@
 	
 	//Property intiialization, using values from options first, then from default values.
+	var atmosphere = options.getfieldvalue('atmosphere', {});
+	canvas.atmosphere = { 												//Default Values
+		wavelength_r: defaultFor(atmosphere.wavelength_r, 			0.65), 		//0.65		Red wavelength (micrometers)
+		wavelength_g: defaultFor(atmosphere.wavelength_g, 			0.57),		//0.57		Green wavelength (micrometers)
+		wavelength_b: defaultFor(atmosphere.wavelength_b, 			0.475),		//0.475		Green wavelength (micrometers)
+		eSun: 	defaultFor(atmosphere.eSun, 						100.0),		//20.0		Sun intensity	
+		kRayleigh: defaultFor(atmosphere.kRayleigh, 				0.0025),	//0.0025	Rayleigh scattering amount
+		kMie: defaultFor(atmosphere.kMie, 							0.000), 	//0.01		Mie scattering amount
+		g: defaultFor(atmosphere.g, 								-0.99),		//-0.99		Mie phase asymmetry/direction factor
+		hdr_exposure: defaultFor(atmosphere.hdr_exposure, 			0.8),		//0.8		High Dynamic Range Exposure
+		scaleHeight: defaultFor(atmosphere.scaleHeight, 			1.25), 		//1.025		Scale of height of atmosphere to earth radius.
+		scaleDepth: defaultFor(atmosphere.scaleDepth, 				0.25), 		//0.25		Percentage altitude at which the atmosphere's average density is found
+		a: defaultFor(atmosphere.a, 								-0.00287),	//-0.00287	Scaling constant a
+		b: defaultFor(atmosphere.b, 								0.459),		//0.459		Scaling constant b
+		c: defaultFor(atmosphere.c, 								3.83),		//3.83		Scaling constant c
+		d: defaultFor(atmosphere.d, 								-6.80),		//-6.80		Scaling constant d
+		e: defaultFor(atmosphere.e, 								3.6),		//5.25		Scaling constant e. Lower when increasing atmosphere scale.
+		attenuation: defaultFor(atmosphere.attenuation, 			0.5)		//0.5		Strength of atmospheric scattering on ground shading.
+	};
+	updateAtmosphereParameters(canvas);
 	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),
-		interval: defaultFor(animation.interval, 1000 / animation.fps),
-		loop: defaultFor(animation.loop, true),
-		handler: defaultFor(animation.handler, 0)
+		frame: defaultFor(animation.frame, 							0),
+		play: defaultFor(animation.play, 							true),
+		increment: defaultFor(animation.increment, 					true),
+		fps: defaultFor(animation.fps, 								4),
+		interval: defaultFor(animation.interval, 					1000 / 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, {})
+		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),
-		hit: defaultFor(clouds.hit, {})
+		position: defaultFor(camera.position, 						vec3.create()),
+		rotation: defaultFor(camera.rotation, 						quat.create()),
+		relativePosition: defaultFor(camera.relativePosition, 		vec3.create()),
+		direction: defaultFor(camera.direction, 					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 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)),
-		reposition: defaultFor(dataMarkers.reposition, true)
+		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)),
+		reposition: defaultFor(dataMarkers.reposition, 				true)
 	};
 	var draw = options.getfieldvalue('draw', {});
 	canvas.draw = {
-		ready: defaultFor(draw.ready, false),
-		handler: defaultFor(draw.handler, null)
+		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)
+		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),
+		preventDefaultOnPan: defaultFor(view.preventDefaultOnPan, 	false),
+		twod: defaultFor(view.twod, 								false)
 	};
 
@@ -171,8 +192,9 @@
 	var shaders = {};
 	shaders.Colored = new GL.Shader.fromURL(rootPath+'shaders/Colored.vsh', rootPath+'shaders/Colored.fsh', null, gl);
+	shaders.ColoredDiffuse = new GL.Shader.fromURL(rootPath+'shaders/ColoredDiffuse.vsh', rootPath+'shaders/ColoredDiffuse.fsh', null, gl);
 	shaders.Textured = new GL.Shader.fromURL(rootPath+'shaders/Textured.vsh', rootPath+'shaders/Textured.fsh', null, gl);
+	shaders.TexturedDiffuse = new GL.Shader.fromURL(rootPath+'shaders/TexturedDiffuse.vsh', rootPath+'shaders/TexturedDiffuse.fsh', null, gl);
 	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;
 } //}}}
@@ -184,4 +206,22 @@
 	return gl.textures[imageSource];
 } //}}}
+function updateAtmosphereParameters(canvas) {
+	//Precalculate derived atmosphere shader parameters
+	//TODO: Find a better way to structure this
+	var atm = canvas.atmosphere;
+	atm.inv_wavelength4 = [1.0 / Math.pow(atm.wavelength_r, 4), 1.0 / Math.pow(atm.wavelength_g, 4), 1.0 / Math.pow(atm.wavelength_b, 4)];
+	atm.innerRadius = 6.371e6;
+	atm.innerRadius2 = atm.innerRadius * atm.innerRadius;
+	atm.outerRadius = atm.innerRadius * atm.scaleHeight;
+	atm.outerRadius2 = atm.outerRadius * atm.outerRadius;
+	atm.krESun = atm.kRayleigh * atm.eSun;
+	atm.kmESun = atm.kMie * atm.eSun;
+	atm.kr4PI = atm.kRayleigh * 4 * Math.PI;
+	atm.km4PI = atm.kMie * 4 * Math.PI;
+	atm.scale = 1.0 / (atm.outerRadius - atm.innerRadius);
+	atm.scaleOverScaleDepth = atm.scale / atm.scaleDepth;
+	atm.g2 = atm.g * atm.g;
+	canvas.atmosphere = atm;
+}
 function clamp(value, min, max) { //{{{
 	return Math.max(min, Math.min(value, max));
@@ -253,47 +293,15 @@
 } //}}}
 //}}}
-//{{{ Interface Functions
+//{{{ Interaction Functions
 function onTap(ev, canvas) { //{{{
-	//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.clouds.enabled) {
-		for (var i = 0; i < canvas.clouds.quantity; i++) {
-			raycast(canvas, ev.srcEvent.layerX, ev.srcEvent.layerY, canvas.nodes["clouds" + i]);
-		}
-	}
-	var hit = raycast(canvas, ev.srcEvent.layerX, ev.srcEvent.layerY, canvas.unitNode);
-	if ('cities' in canvas.nodes) {
-		var hitCities = raycast(canvas, ev.srcEvent.layerX, ev.srcEvent.layerY, canvas.nodes.cities);
-		canvas.nodes.cities.hit = hitCities;
-		updateCities(canvas);
-	}
-
-	canvas.brush.hit = hit;
-	
-	if (canvas.dataMarkers.enabled) {
-		canvas.dataMarkers.marker.selector.closed = false;
-		canvas.dataMarkers.marker.hit = hit;
-		updateMarker(canvas, true);
-	}
-	
-	brushModify(canvas);
+	var hit = raycastXY(canvas, ev.srcEvent.layerX, ev.srcEvent.layerY, canvas.unitNode);
+
+	//Trigger any handlers attatched to this canvas event.
+	canvas.selector.trigger('onTap', [ev, canvas, hit]);
 } //}}}
 function onPan(ev, canvas, displaylog) { //{{{
 	ev.preventDefault();
-	
-	if (canvas.dataMarkers.enabled) {
-		if (!isEmptyOrUndefined(canvas.unitNode)) {
-			canvas.brush.hit = raycast(canvas, ev.srcEvent.layerX, ev.srcEvent.layerY, canvas.unitNode);
-			brushModify(canvas);
-		}
-	}
-	
-	if (canvas.clouds.enabled) {
-		if (!isEmptyOrUndefined(canvas.nodes['overlay'])) {
-			canvas.clouds.hit = raycast(canvas, ev.srcEvent.layerX, ev.srcEvent.layerY, canvas.nodes['overlay']);
-			updateClouds(canvas);
-		}
-	}
 	
 	if (ev.type === 'panstart') {
@@ -301,55 +309,74 @@
 		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;
+	
+	//Trigger any handlers attatched to this canvas event.
+	canvas.selector.trigger('onPan', [ev, canvas]);
+	
+	//If any onPan handler sets preventDefaultOnPan to true, skips default onPan camera behavior
+	if (!canvas.view.preventDefaultOnPan) {
+		//If panning with two fingers or shift key, translate camera center
+		if (ev.srcEvent.shiftKey || ev.pointers.length === 2) {
+			if (canvas.view.panningEnabled) {
+				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;
+				
+				//TODO: convert canvas.view.rotation from az/el euler to quaternion
+				if (canvas.view.twod) {
+					canvas.view.position[0] += Math.cos(DEG2RAD * canvas.view.rotation[0]) * deltaX - Math.sin(DEG2RAD * 0) * deltaY;
+					canvas.view.position[2] += Math.sin(DEG2RAD * canvas.view.rotation[0]) * deltaX + Math.cos(DEG2RAD * 0) * deltaY;
+				}
+				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, rotate around camera center
 		else {
-			canvas.view.position[0] += Math.cos(DEG2RAD * canvas.view.rotation[0]) * deltaX - Math.sin(DEG2RAD * canvas.view.rotation[0]) * deltaY;
-			canvas.view.position[2] += Math.sin(DEG2RAD * canvas.view.rotation[0]) * deltaX + Math.cos(DEG2RAD * canvas.view.rotation[0]) * deltaY;
+			canvas.view.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]);
+			
+			if (displaylog) { console.log(canvas.view.rotation); }
 		}
-	}
-	
-	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.view.preventDefaultOnPan = false;
 	canvas.lastDeltaX = ev.deltaX;
 	canvas.lastDeltaY = ev.deltaY;
-	
-	canvas.dataMarkers.reposition = true;
-	
-	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); }
+	else { modifyZoom(ev.scale * canvas.view.lastZoom, canvas, displaylog, ev, 0); }
 } //}}}
 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]);
-	canvas.dataMarkers.reposition = true;
-	if (displaylog) { console.log(canvas.view.zoom); }
-} //}}}
-function modifyDataMarkersEnabled(value, canvas) { //{{{
-	canvas.dataMarkers.enabled = value;
+	var delta = clamp(ev.scale || ev.wheelDelta || -ev.detail, -1, 1) * canvas.controlSensitivity * canvas.view.zoom / 2;
+	modifyZoom(canvas.view.zoom + delta, canvas, displaylog, ev, 0);
+} //}}}
+function modifyZoom(value, canvas, displaylog, ev, duration) { //{{{
+	if (isEmptyOrUndefined(duration)) duration = 200;
+	var targetZoom = clamp(value, canvas.view.zoomLimits[0], canvas.view.zoomLimits[1]);
+	var currentZoom = canvas.view.zoom;
+	animateValue(
+		0,
+		1.0,
+		duration,
+		function(value, info) {
+			canvas.view.zoom = currentZoom * (1 - value) + targetZoom * value;
+			if (displaylog) { console.log(canvas.view.zoom); }
+			
+			//Trigger any handlers attatched to this canvas event.
+			canvas.selector.trigger('onZoom', [ev, canvas]);
+		}
+	);
 } //}}}
 function toggleMoviePlay(canvas) { //{{{
@@ -364,288 +391,39 @@
 	}
 } //}}}
-//}}}
-//{{{ Interaction Functions
-function raycast(canvas, x, y, node) { //{{{
-	//Performs raycast on canvas.unitNode.mesh using x/y screen coordinates. Returns hit objects with hit position, coords, and indicies of ray-triangle intersection.
+function screenToWorldPoint(canvas, x, y) { //{{{
+	var viewportX = (x - canvas.width / 2) / (canvas.width / 2);
+	var viewportY = (canvas.height / 2 - y) / (canvas.height / 2);
+	var origin = vec3.transformMat4(vec3.create(), [viewportX, viewportY, 0], canvas.camera.vpInverseMatrix);
+	return origin;
+} //}}}
+function screenToModelRay(canvas, x, y, node) { //{{{
+	var inverseMVPMatrix = mat4.invert(mat4.create(), mat4.multiply(mat4.create(), canvas.camera.vpMatrix, node.modelMatrix));
+	var viewportX = (x - canvas.width / 2) / (canvas.width / 2);
+	var viewportY = (canvas.height / 2 - y) / (canvas.height / 2);
+	var origin = vec3.transformMat4(vec3.create(), [viewportX, viewportY, 0], inverseMVPMatrix);
+	var far = vec3.transformMat4(vec3.create(), [viewportX, viewportY, 1.0], inverseMVPMatrix);
+	var direction = vec3.subtract(vec3.create(), far, origin);
+	return {'origin':origin, 'direction':direction};
+} //}}}
+function raycast(canvas, origin, direction, node) { //{{{
+	//Performs raycast on given node using ray origin and direction vectors.
+	//Returns hit objects with hit position, normals, barycentric coordinates, element number, and indices of ray-triangle intersection.
 	//TODO: Diagnose marker issues with orthographic views and slr-eustatic updates when switching between basins.
-	var inverseMVPMatrix = mat4.invert(mat4.create(), mat4.multiply(mat4.create(), canvas.camera.vpMatrix, node.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 = node.mesh;
-
-	if (!mesh) { return; }
-	if (!node.octree) { node.octree = new GL.Octree(mesh); }
-	
-	var hit = node.octree.testRay(origin, ray, 1e3, 1e10);
-	
-	if(!hit) { return; }
-
-	if (node.name.startsWith("clouds")) { canvas.clouds.selected = node.name; alert("New selected cloud: " + canvas.clouds.selected); }
+	if (!node.octree) { node.octree = new GL.Octree(node.mesh); }
+	
+	var hit = node.octree.testRay(origin, direction, 1e3, 1e10);
+	if (!hit) { return; }
 
 	hit.modelPos = vec3.copy(vec3.create(), hit.pos);
 	vec3.transformMat4(hit.pos, hit.pos, node.modelMatrix);
-	vec3.transformMat4(hit.normal, hit.normal, node.modelMatrix);
-
+	
 	return hit;
 } //}}}
-function updateCities(canvas) {
-	//Update selected city
-	var hit = canvas.nodes.cities.hit;
-	if (hit) {
-		citiesIndex = Math.floor(hit.indices[0] / ((ArrayMax(canvas.nodes.cities.mesh.getIndexBuffer('triangles').data) + 1) / cities.length));
-		cityName = cities[citiesIndex];
-		if (cityName !== $('#gfm-sim-controls-select-city').val()) {
-			$('#gfm-sim-controls-select-city').val(cityName).selectmenu("refresh");
-			changeCity(canvas);
-		}
-	}
-}
-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.
-	//TODO: Allow variable brush size/additional neighbors. Allow the function to work on multiple models (currently hardcoded to md model).
-	if (!canvas.unitNode || canvas.brush.enabled != 'on') { return; }
-	
-	var hit = canvas.brush.hit;
-
-	if (hit) {
-		var bufferVertices = canvas.unitNode.mesh.getBuffer('vertices');
-		var vertices = bufferVertices.data;
-		var bufferCoords = canvas.unitNode.mesh.getBuffer('coords');
-		var coords = bufferCoords.data;
-
-		//Query nearby elements and store indicies of affected vertices using pregenerated vertexconnectivity list (from NodeConnectivity)
-		var baseIndices = new Set(hit.indices);
-		var connectedIndices = new Set(hit.indices);
-		var connectedElement;
-		var indices;
-		var lengthIndex = md.mesh.vertexconnectivity[0].length - 1;
-		var length;
-		for (var i = 0; i < 3; i++) {
-			length = md.mesh.vertexconnectivity[hit.indices[i]][lengthIndex];
-			for (var j = 0; j < length; j++) {
-				//Shift elements down by one (matlab 1-based index to 0-based index)
-				connectedElement = md.mesh.vertexconnectivity[hit.indices[i]][j] - 1;
-				indices = md.mesh.elements[connectedElement];
-				connectedIndices.add(indices[0] - 1);
-				connectedIndices.add(indices[1] - 1);
-				connectedIndices.add(indices[2] - 1);
-			}
-		}
-
-		//Apply modifications to included vertices in mesh using brush strength and falloff.
-		var strength;
-		for (var index of connectedIndices) {
-			if (!baseIndices.has(index)) {
-				strength = canvas.brush.strength * canvas.brush.falloff;
-			}
-			else {
-				strength = canvas.brush.strength;
-			}
-			vertices[index*3+2] += strength * 100;
-			md.geometry.surface[index] += strength;	
-			md.geometry.thickness[index] += strength;
-			coords[index*2+1] += strength;
-			canvas.unitData[index] += strength;
-		}
-		
-		//Update mesh on GPU
-		bufferVertices.upload(canvas.gl.DYNAMIC_DRAW);
-		bufferCoords.upload(canvas.gl.DYNAMIC_DRAW);
-		canvas.unitNode.octree = new GL.Octree(canvas.unitNode.mesh);	
-	}
-} //}}}
-function updateClouds(canvas) {
-	//Update clouds if rendered
-	//TODO: Steven, the hit now queries the radaroverlay.
-	if (canvas.nodes[canvas.clouds.selected]) {
-		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);//move out of brushModify, perhaps onto onPan
-		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[canvas.clouds.selected].translation = [x, y + canvas.clouds.height, z];
-		updateModelMatrix(canvas.nodes[canvas.clouds.selected]);
-	}
-}
-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');
-			marker.closed = true;
-		});
-		marker.closed = false;
-		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.
-	//Return if no marker hit exists, the camera is not rendering, or if no reposition has been scheduled.
-	if (isEmptyOrUndefined(canvas.dataMarkers.marker.hit) || !canvas.camera.ready || !canvas.dataMarkers.reposition) { return; }
-	var size = canvas.dataMarkers.size;
-	var screenPoint = vec3.transformMat4(vec3.create(), canvas.dataMarkers.marker.hit.pos, canvas.camera.vpMatrix);
-	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'); }
-	canvas.dataMarkers.reposition = false;
-} //}}}
-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 (!canvas.dataMarkers.enabled || isEmptyOrUndefined(canvas.dataMarkers.marker.hit)) { return; }
-	if (isEmptyOrUndefined(canvas.unitNode) || isEmptyOrUndefined(canvas.unitNode.mesh)) { 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;
-	var longitude = md.mesh.long;
-	var thickness;
-	var velocity;
-	if (md.results[0]) {
-		thickness = md.results[canvas.animation.frame].Thickness;
-		velocity = md.results[canvas.animation.frame].Vel;
-	}
-	else {
-		thickness = md.geometry.thickness;
-		velocity = md.initialization.vel;
-	}
-	
-	//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 labels = canvas.dataMarkers.labels.slice();
-	for (var i = 0; i < labels.length; i++) {
-		if (labels[i].toLowerCase() === 'x') { labels[i] = hit.modelPos[0]; }
-		else if (labels[i].toLowerCase() === 'y') { labels[i] = hit.modelPos[1]; }
-		else if (labels[i].toLowerCase() === 'z') { labels[i] = hit.modelPos[2]; }
-		else if (labels[i].toLowerCase() === 'latitude') {
-			var hitLatitude = [latitude[hit.indices[0]], latitude[hit.indices[1]], latitude[hit.indices[2]]];
-			var valueLatitude = Math.abs(hitLatitude[0] * hit.uvw[0] + hitLatitude[1] * hit.uvw[1] + hitLatitude[2] * hit.uvw[2]);
-			labels[i] = valueLatitude;
-		}
-		else if (labels[i].toLowerCase() === 'longitude') {
-			var hitLongitude = [longitude[hit.indices[0]], longitude[hit.indices[1]], longitude[hit.indices[2]]];
-			var valueLongitude = Math.abs(hitLongitude[0] * hit.uvw[0] + hitLongitude[1] * hit.uvw[1] + hitLongitude[2] * hit.uvw[2]);
-			labels[i] = valueLongitude;
-		}
-		else if (labels[i].toLowerCase() === 'thickness') {
-			var hitThickness = [thickness[hit.indices[0]], thickness[hit.indices[1]], thickness[hit.indices[2]]];
-			var valueThickness = hitThickness[0] * hit.uvw[0] + hitThickness[1] * hit.uvw[1] + hitThickness[2] * hit.uvw[2];
-			labels[i] = valueThickness; 
-		}
-		else if (labels[i].toLowerCase() === 'velocity') {
-			var hitVelocity = [velocity[hit.indices[0]], velocity[hit.indices[1]], velocity[hit.indices[2]]];
-			var valueVelocity = hitVelocity[0] * hit.uvw[0] + hitVelocity[1] * hit.uvw[1] + hitVelocity[2] * hit.uvw[2];	
-			labels[i] = valueVelocity; 
-		}
-		else if (labels[i].toLowerCase() === 'value') {
-			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;
-			labels[i] = value; 
-		}
-	}
-	
-	//Apply changes to tooltip
-	$('#tooltip-content-data-marker-' + canvas.id).html(sprintf.apply(null, canvas.dataMarkers.format.concat(labels)));
-	$('#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.unitMovieData[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 (canvas.animation.lastFrame > canvas.animation.frame) {
-				canvas.dataMarkers.values = [];
-			}
-			if (isEmpty || newMovieFrame) {
-				coords = canvas.unitMovieData[canvas.animation.frame];
-				var hitValues = [coords[hit.indices[0]], coords[hit.indices[1]], coords[hit.indices[2]]];
-				var value = hitValues[0] * hit.uvw[0] + hitValues[1] * hit.uvw[1] + hitValues[2] * hit.uvw[2];	
-				canvas.dataMarkers.values.push([canvas.animation.frame, value]);
-			}
-		}
-		
-		//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
-			);
-		}
-	}
-	canvas.dataMarkers.reposition = true;
-	if (reset && !canvas.dataMarkers.marker.selector.closed) {
-		canvas.dataMarkers.marker.selector.fadeIn(175);
-		canvas.dataMarkers.marker.selector.tooltipster('open');
-	}
+function raycastXY(canvas, x, y, node) { //{{{
+	//Performs raycast on given node using x and y screenspace coordinates.
+	//Returns hit objects with hit position, normals, barycentric coordinates, element number, and indices of ray-triangle intersection.
+	//TODO: Diagnose marker issues with orthographic views and slr-eustatic updates when switching between basins.
+	var ray = screenToModelRay(canvas, x, y, node);
+	return raycast(canvas, ray.origin, ray.direction, node);
 } //}}}
 function animateValue(current, target, duration, stepCallback, doneCallback) { //{{{
@@ -669,14 +447,15 @@
 	var elevationRotationMatrix = mat4.create();
 	var aspectRatio = canvas.clientWidth / canvas.clientHeight;
-	var cameraPosition = vec3.create();
-
-	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); }
+	var camera = canvas.camera;
+	var view = canvas.view;
+
+	if (view.twod) { mat4.ortho(pMatrix, -aspectRatio*6.371e6/view.zoom, aspectRatio*6.371e6/view.zoom, -6.371e6/view.zoom, 6.371e6/view.zoom, camera.near, camera.far); }
+	else { mat4.perspective(pMatrix, camera.fov * DEG2RAD, aspectRatio, camera.near, camera.far); }
 	
 	//Apply worldspace translation
-	mat4.translate(vMatrix, translateMatrix, vec3.negate(vec3.create(), canvas.view.position));
+	mat4.translate(vMatrix, translateMatrix, vec3.negate(vec3.create(), view.position));
 	
 	//Calculate rotation around camera focal point about worldspace origin
-	if (canvas.view.twod) {
+	if (view.twod) {
 		mat4.rotate(azimuthRotationMatrix, azimuthRotationMatrix, -DEG2RAD * 0, [0, 1, 0]);
 		mat4.rotate(elevationRotationMatrix, elevationRotationMatrix, DEG2RAD * 90, [1, 0, 0]);
@@ -684,7 +463,13 @@
 	}
 	else {
-		mat4.rotate(azimuthRotationMatrix, azimuthRotationMatrix, -DEG2RAD * (canvas.view.rotation[0] + 90), [0, 1, 0]);
-		mat4.rotate(elevationRotationMatrix, elevationRotationMatrix, DEG2RAD * canvas.view.rotation[1], [1, 0, 0]);
+		mat4.rotate(azimuthRotationMatrix, azimuthRotationMatrix, -DEG2RAD * (view.rotation[0] + 90), [0, 1, 0]);
+		mat4.rotate(elevationRotationMatrix, elevationRotationMatrix, DEG2RAD * view.rotation[1], [1, 0, 0]);
 		mat4.multiply(rotationMatrix, elevationRotationMatrix, azimuthRotationMatrix);
+		//var quaternionWorldX = Node.prototype.eulerToQuaternion(0, 0, DEG2RAD * (view.rotation[0]));
+		//var quaternionWorldY = Node.prototype.eulerToQuaternion(0, DEG2RAD * (view.rotation[1]), 0);
+		//var quaternionWorldZ = Node.prototype.eulerToQuaternion(DEG2RAD * (view.rotation[2]), 0, 0);
+		//var quaternionTemp = quat.multiply(quat.create(), quaternionWorldY, quaternionWorldX);
+		//quat.multiply(camera.rotation, quaternionWorldZ, quaternionTemp);
+		//mat4.fromQuat(rotationMatrix, camera.rotation);	
 	}
 
@@ -694,18 +479,20 @@
 	//Apply screenspace translation to emulate rotation around point
 	mat4.identity(translateMatrix);
-	mat4.translate(translateMatrix, translateMatrix, [0.0, 0.0, -6.371e6/canvas.view.zoom]);
+	mat4.translate(translateMatrix, translateMatrix, [0.0, 0.0, -6.371e6/view.zoom]);
 	mat4.multiply(vMatrix, translateMatrix, vMatrix);
 	
 	//Apply projection matrix to get camera matrix
-	mat4.copy(canvas.camera.vMatrix, vMatrix);
-	mat4.multiply(canvas.camera.vpMatrix, pMatrix, vMatrix);
+	mat4.copy(camera.vMatrix, vMatrix);
+	mat4.multiply(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;
-	repositionMarker(canvas);
+	mat4.invert(camera.vInverseMatrix, camera.vMatrix);
+	mat4.invert(camera.vpInverseMatrix, camera.vpMatrix);
+	
+	vec3.transformMat4(camera.position, vec3.create(), camera.vInverseMatrix);
+	vec3.sub(camera.relativePosition, camera.position, view.position);
+	vec3.normalize(camera.direction, camera.relativePosition);
+	
+	camera.ready = true;
 }//}}}
 function drawSceneGraphNode(canvas, node) { //{{{
@@ -718,10 +505,9 @@
 	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, node.modelMatrix);
-	mat4.transpose(normalMatrix, normalMatrix);
+	var normalMatrix = mat3.create();
+	var tempMatrix = mat4.create();
+	mat4.invert(tempMatrix, node.modelMatrix);
+	mat4.transpose(tempMatrix, tempMatrix);
+	mat3.fromMat4(normalMatrix, tempMatrix);
 	
 	if (node.texture) { node.texture.bind(0); }
@@ -732,81 +518,56 @@
 	gl.lineWidth(node.lineWidth);
 	gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
-
+	
 	//Setup for light that originates from camera
-	var origin = vec3.fromValues(0, 0, 0);
-	var lightOrigin = vec3.fromValues(0, 0, 0);
-	var cameraPositionRelative = vec3.create();
-	vec3.transformMat4(origin, origin, canvas.camera.vInverseMatrix);
-	vec3.normalize(lightOrigin, lightOrigin);
-	vec3.sub(cameraPositionRelative, origin, node.translation);
-	cameraHeight = vec3.length(cameraPositionRelative);
-	
-	var atm = { 					//Default Values
-		wavelength_r: 0.65, 		//0.65		Red wavelength (micrometers)
-		wavelength_g: 0.57,			//0.57		Green wavelength (micrometers)
-		wavelength_b: 0.475,		//0.475		Green wavelength (micrometers)
-		eSun: 100.0,				//20.0		Sun intensity	
-		kRayleigh: 0.0025,			//0.0025	Rayleigh scattering amount
-		kMie: 0.000, 				//0.01		Mie scattering amount
-		g: -0.99,					//-0.99		Mie phase asymmetry/direction factor
-		hdr_exposure: 0.8,			//0.8		High Dynamic Range Exposure
-		scale: 1.25, 				//1.025		Scale of atmosphere. WARNING: Change atmosphereScale in applyoptions.js, and scaling constants.
-		scaleDepth: 0.25, 			//0.25		Percentage altitude at which the atmosphere's average density is found
-		a: -0.00287,				//-0.00287	Scaling constant a
-		b: 0.459,					//0.459		Scaling constant b
-		c: 3.83,					//3.83		Scaling constant c
-		d: -6.80,					//-6.80		Scaling constant d
-		e: 3.6,						//5.25		Scaling constant e. Lower when increasing atmosphere scale.
-		attenuation: 0.5			//0.5		Strength of atmospheric scattering on ground shading.
-	};
-			
-	var inv_wavelength4 = [1.0 / Math.pow(atm.wavelength_r, 4), 1.0 / Math.pow(atm.wavelength_g, 4), 1.0 / Math.pow(atm.wavelength_b, 4)];
-	var innerRadius = 6.371e6;
-	var outerRadius = innerRadius*atm.scale;
-	var scale = 1.0 / (outerRadius - innerRadius);
-	var scaleDepth = atm.scaleDepth;
+	var atm = canvas.atmosphere;
+	var lightOrigin = vec3.create();
 	
 	node.shader.uniforms({
-		m4MVP: mvpMatrix,
-		m4Normal: normalMatrix,
-		m4Model: node.modelMatrix,
-		u_lightPosition: lightOrigin,
-		//u_lightPosition: [1.0, 1.0, 1.0],
-		u_diffuseColor: node.diffuseColor,
-		u_texture: 0,
-		u_alpha: node.alpha,
-		u_maskZerosColor: node.maskZerosColor,
-		u_maskZerosEnabled: node.maskZerosEnabled,
-		u_maskZerosTolerance: node.maskZerosTolerance,
-		u_maskZerosZeroValue: node.maskZerosZeroValue,
-		u_maskEnabled: node.maskEnabled,
-		u_maskHeight: node.maskHeight,
-		u_maskColor: node.maskColor,
-		u_pointSize: node.pointSize,
-		v3CameraPosition: origin,
-		v3Translate: node.translation,
-		v3LightPos: lightOrigin,
-		v3InvWavelength: inv_wavelength4,
-		fOuterRadius: outerRadius,
-		fOuterRadius2: outerRadius * outerRadius,
-		fInnerRadius: innerRadius,
-		fInnerRadius2: innerRadius * innerRadius,
-		fKrESun: atm.kRayleigh * atm.eSun, 
-		fKmESun: atm.kMie * atm.eSun, 
-		fKr4PI: atm.kRayleigh * 4 * Math.PI, 
-		fKm4PI: atm.kMie * 4 * Math.PI,
-		fScale: scale, 
-		fScaleDepth: scaleDepth,
-		fScaleOverScaleDepth: scale/scaleDepth, 
-		v3LightPosFrag: lightOrigin,
-		fHdrExposure: atm.hdr_exposure,	
-		g: atm.g,			
-		g2: atm.g * atm.g,
-		a: atm.a,
-		b: atm.b,
-		c: atm.c,
-		d: atm.d,		
-		e: atm.e,
-		attenuation: atm.attenuation
+		m4MVP: 					mvpMatrix,
+		m3Normal: 				normalMatrix,
+		m4Model: 				node.modelMatrix,
+		u_alpha: 				node.alpha,
+		u_ambientColor: 		node.ambientColor,
+		u_cameraPosition: 		canvas.camera.position,
+		u_diffuseColor: 		node.diffuseColor,
+		u_lightDirection: 		canvas.camera.direction,
+		u_lightingBias: 		node.lightingBias,
+		u_maskZerosColor: 		node.maskZerosColor,
+		u_maskZerosEnabled: 	node.maskZerosEnabled,
+		u_maskZerosTolerance: 	node.maskZerosTolerance,
+		u_maskZerosZeroValue: 	node.maskZerosZeroValue,
+		u_maskEnabled: 			node.maskEnabled,
+		u_maskHeight: 			node.maskHeight,
+		u_maskColor: 			node.maskColor,
+		u_pointSize: 			node.pointSize,
+		u_specularColor: 		node.specularColor,
+		u_specularPower: 		node.specularPower,
+		u_specularStrength: 	node.specularStrength,
+		u_texture: 				0,
+		v3CameraPosition: 		canvas.camera.position,
+		v3Translate: 			node.translation,
+		v3LightPos: 			lightOrigin,
+		v3InvWavelength: 		atm.inv_wavelength4,
+		fOuterRadius: 			atm.outerRadius,
+		fOuterRadius2: 			atm.outerRadius2,
+		fInnerRadius: 			atm.innerRadius,
+		fInnerRadius2: 			atm.innerRadius2,
+		fKrESun: 				atm.krESun,
+		fKmESun: 				atm.kmESun,
+		fKr4PI: 				atm.kr4PI,
+		fKm4PI: 				atm.km4PI,
+		fScale: 				atm.scale, 
+		fScaleDepth: 			atm.scaleDepth,
+		fScaleOverScaleDepth: 	atm.scaleOverScaleDepth, 
+		v3LightPosFrag: 		lightOrigin,
+		fHdrExposure: 			atm.hdr_exposure,	
+		g: 						atm.g,			
+		g2: 					atm.g2,
+		a: 						atm.a,
+		b: 						atm.b,
+		c: 						atm.c,
+		d: 						atm.d,		
+		e: 						atm.e,
+		attenuation: 			atm.attenuation
 	}).draw(node.mesh, node.drawMode, 'triangles');
 	
@@ -814,4 +575,16 @@
 	gl.disable(gl.CULL_FACE);
 } //}}}
+function canvasResize(canvas) {
+	var rect = canvas.getBoundingClientRect();
+	canvas.width  = rect.width;
+	canvas.height = rect.height;
+	canvas.gl.viewport(0, 0, canvas.width, canvas.height);
+	
+	if (!isEmptyOrUndefined(canvas.overlaycanvas)) {
+		rect = canvas.overlaycanvas.getBoundingClientRect();
+		canvas.overlaycanvas.width  = rect.width;
+		canvas.overlaycanvas.height = rect.height;
+	}
+}
 function draw(canvas) { //{{{
 	//Ensure all nodes are ready to render
@@ -833,11 +606,9 @@
 	//Begin rendering nodes
 	if (canvas.draw.ready) {
-		var rect = canvas.getBoundingClientRect();
-		canvas.width  = rect.width;
-		canvas.height = rect.height;
+		//Handle canvas resizing and viewport/screenspace coordinate synchronization
+		canvasResize(canvas);	
 		
 		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);
@@ -845,6 +616,8 @@
 		updateCameraMatrix(canvas);
 		
-		if (canvas.textcanvas) { canvas.textcanvas.draw(canvas); }
-		if (canvas.overlaycanvas) { canvas.overlaycanvas.draw(canvas); }
+		//Trigger any handlers attatched to this canvas event.
+		canvas.selector.trigger('onPreRender', [canvas]);
+		
+		for (var handler in canvas.overlayHandlers) { canvas.overlayHandlers[handler](canvas); }
 		
 		var drawPassNumber = 3;
Index: issm/trunk-jpl/src/m/plot/webgl_node.js
===================================================================
--- issm/trunk-jpl/src/m/plot/webgl_node.js	(revision 21828)
+++ issm/trunk-jpl/src/m/plot/webgl_node.js	(revision 21911)
@@ -6,5 +6,5 @@
 
 function Node() { //{{{
-	//properties 
+	//properties
 	// {{{
 	var args = Array.prototype.slice.call(arguments);
@@ -15,49 +15,93 @@
 	this.gl = canvas.gl;
 	
-	this.alpha = options.getfieldvalue('alpha', 								1.0),
-	this.animation = options.getfieldvalue('animation', 						{}),
-	this.arrays = options.getfieldvalue('arrays', 								{}),
-	this.caxis = options.getfieldvalue('caxis', 								[0.0, 1.0]),
-	this.center = options.getfieldvalue('center', 								vec3.create()),
-	this.cullFace = options.getfieldvalue('cullFace', 							this.gl.BACK),
-	this.computeIndices = options.getfieldvalue('computeIndices', 				true),
-	this.disableDepthTest = options.getfieldvalue('disableDepthTest', 			false),
-	this.diffuseColor = options.getfieldvalue('diffuseColor', 					[0.0, 0.0, 0.0, 1.0]),
-	this.drawMode = options.getfieldvalue('drawMode', 							this.gl.TRIANGLES),
-	this.drawOrder = options.getfieldvalue('drawOrder', 						1),
-	this.enabled = options.getfieldvalue('enabled', 							true),
-	this.enableCullFace = options.getfieldvalue('enableCullFace', 				true),
-	this.hideOcean = options.getfieldvalue('hideOcean', 						false),
-	this.lineWidth = options.getfieldvalue('lineWidth', 						1.0),
-	this.log = options.getfieldvalue('log', 									false),
-	this.maskColor = options.getfieldvalue('maskColor', 						vec4.fromValues(0.0, 0.0, 1.0, 1.0)),
-	this.maskEnabled = options.getfieldvalue('maskEnabled', 					false),
-	this.maskHeight = options.getfieldvalue('maskHeight', 						150.0),
-	this.maskZerosColor = options.getfieldvalue('maskZerosColor', 				[1.0, 1.0, 1.0, 1.0]),
-	this.maskZerosEnabled = options.getfieldvalue('maskZerosEnabled', 			false),
-	this.maskZerosTolerance = options.getfieldvalue('maskZerosTolerance', 		1e-6),
-	this.maskZerosZeroValue = options.getfieldvalue('maskZerosZeroValue', 		0.5),
-	this.mesh = options.getfieldvalue('mesh', 									undefined),
-	this.name = options.getfieldvalue('name', 									'node'),
-	this.nanIndices = options.getfieldvalue('nanIndices', 						new Set()),
-	this.octree = options.getfieldvalue('octree', 								undefined),
-	this.pointSize = options.getfieldvalue('pointSize', 						15.0),
-	this.shaderName = options.getfieldvalue('shaderName', 						'Colored'),
-	this.shader = options.getfieldvalue('shader', 								this.gl.shaders[this.shaderName]),
-	this.texture = options.getfieldvalue('texture', 							undefined),
-	this.scale = options.getfieldvalue('scale', 								vec3.fromValues(1, 1, 1)),
-	this.rotation = options.getfieldvalue('rotation', 							vec3.create()),
-	this.translation = options.getfieldvalue('translation', 					vec3.create()),
-	this.modelMatrix = options.getfieldvalue('modelMatrix', 					mat4.create()),
-	this.rotationMatrix = options.getfieldvalue('rotationMatrix', 				mat4.create()),
-	this.inverseModelMatrix = options.getfieldvalue('inverseModelMatrix', 		mat4.create()),
-	this.inverseRotationMatrix = options.getfieldvalue('inverseRotationMatrix', mat4.create())
+	this.alpha = options.getfieldvalue('alpha', 								1.0);									//Shading transparency.
+	this.ambientColor = options.getfieldvalue('ambientColor', 					[0.0, 0.0, 0.0, 1.0]);					//Ambient color used in lighting.
+	this.animation = options.getfieldvalue('animation', 						{});									//Animtation parameters.
+	this.arrays = options.getfieldvalue('arrays', 								{});									//Storage for webgl arrays.
+	this.caxis = options.getfieldvalue('caxis', 								[0.0, 1.0]);							//Color axis for texturing.
+	this.center = options.getfieldvalue('center', 								vec3.create());							//Center of rotation/scaling.
+	this.cullFace = options.getfieldvalue('cullFace', 							this.gl.BACK);							//GL enum for face culling back/front faces.
+	this.computeIndices = options.getfieldvalue('computeIndices', 				true);									//Specifies whether or not indices need to be computed during patching.
+	this.disableDepthTest = options.getfieldvalue('disableDepthTest', 			false);									//GL enum for enabling/disabling depth testing.
+	this.diffuseColor = options.getfieldvalue('diffuseColor', 					[0.0, 0.0, 0.0, 1.0]);					//Diffuse color in vec4 rgba format.
+	this.drawMode = options.getfieldvalue('drawMode', 							this.gl.TRIANGLES);						//GL enum for draw mode.
+	this.drawOrder = options.getfieldvalue('drawOrder', 						1);										//Drawing order for non-depth tested/transparent objects. Higher numbers are drawn. first.
+	this.enabled = options.getfieldvalue('enabled', 							true);									//Toggles display of this nodde.
+	this.enableCullFace = options.getfieldvalue('enableCullFace', 				true);									//Toggles use of face culling.
+	this.hideOcean = options.getfieldvalue('hideOcean', 						false);									//ISSM shader uniform controlling ocean masking
+	this.lineWidth = options.getfieldvalue('lineWidth', 						1.0);									//Controls width of gl lines. No reliable support across windows platforms.
+	this.lightingBias = options.getfieldvalue('lightingBias', 					0.0);									//Controls width of gl lines. No reliable support across windows platforms.
+	this.log = options.getfieldvalue('log', 									false);									//Controls logarithmic color axis scaling for texturing.
+	this.maskColor = options.getfieldvalue('maskColor', 						vec4.fromValues(0.0, 0.0, 1.0, 1.0));	//ISSM shader uniform controlling ocean masking color.
+	this.maskEnabled = options.getfieldvalue('maskEnabled', 					false);									//ISSM shader uniform toggling ocean masking.
+	this.maskHeight = options.getfieldvalue('maskHeight', 						150.0);									//ISSM shader uniform controlling height at which ocean masking is cut off.
+	this.maskZerosColor = options.getfieldvalue('maskZerosColor', 				[1.0, 1.0, 1.0, 1.0]);					//ISSM shader uniform controlling value masking color.
+	this.maskZerosEnabled = options.getfieldvalue('maskZerosEnabled', 			false);									//ISSM shader uniform toggling value masking.
+	this.maskZerosTolerance = options.getfieldvalue('maskZerosTolerance', 		1e-6);									//ISSM shader uniform controlling tolerance of value to zero value when masking.
+	this.maskZerosZeroValue = options.getfieldvalue('maskZerosZeroValue', 		0.5);									//ISSM shader uniform controlling zero value masking offset.
+	this.mesh = options.getfieldvalue('mesh', 									undefined);								//Litegl GL.Mesh class used for drawing.
+	this.name = options.getfieldvalue('name', 									'node');								//Name of node.
+	this.nanIndices = options.getfieldvalue('nanIndices', 						new Set());								//Set of NaN indices to mask out of mesh.
+	this.octree = options.getfieldvalue('octree', 								undefined);								//Fast raytracing octree object.
+	this.pointSize = options.getfieldvalue('pointSize', 						15.0);									//Size of points when displaying GL.POINTS.
+	this.shaderName = options.getfieldvalue('shaderName', 						'Colored');								//Name of shader to use.
+	this.shader = options.getfieldvalue('shader', 								this.gl.shaders[this.shaderName]);		//Compiled shader object.
+	this.specularColor = options.getfieldvalue('specularColor', 				[1.0, 1.0, 1.0, 1.0]);					//Specular reflection color used in lighting.
+	this.specularStrength = options.getfieldvalue('specularStrength', 			1.0);									//Specular reflection power - represents specular highlight sharpness.
+	this.specularPower = options.getfieldvalue('specularPower', 				5);										//Specular reflection power - represents specular highlight sharpness.
+	this.texture = options.getfieldvalue('texture', 							undefined);								//GL texture object.
+	this.scale = options.getfieldvalue('scale', 								vec3.fromValues(1, 1, 1));				//XYZ scaling of the node.
+	this.renderObject = options.getfieldvalue('renderObject', 					{});									//Field for additional render object state information not necessarily related to node display.
+	this.rotation = options.getfieldvalue('rotation', 							vec3.create());							//XYZ rotation of the node.
+	this.rotationQuaternion = options.getfieldvalue('rotationQuaternion',		quat.create());							//Quaternion rotation of the node. Not currently in use.
+	this.translation = options.getfieldvalue('translation', 					vec3.create());							//XYZ translation of the node.
+	this.modelMatrix = options.getfieldvalue('modelMatrix', 					mat4.create());							//Model matrix computed from updateModelMatrix function.
+	this.translationMatrix = options.getfieldvalue('translationMatrix', 		mat4.create());							//Intermediate translation matrix computed from updateModelMatrix function.
+	this.rotationMatrix = options.getfieldvalue('rotationMatrix', 				mat4.create());							//Intermediate rotation matrix computed from updateModelMatrix function.
+	this.scaleMatrix = options.getfieldvalue('scaleMatrix', 					mat4.create());							//Intermediate scale matrix computed from updateModelMatrix function.
+	this.inverseModelMatrix = options.getfieldvalue('inverseModelMatrix', 		mat4.create());							//Inverse model matrix.
+	this.inverseRotationMatrix = options.getfieldvalue('inverseRotationMatrix', mat4.create());							//Inverse rotation matrix.
 	//}}}
 	//initialize {{{
-	//if (this.name in canvas.nodes) abort? 
+	//if (this.name in canvas.nodes) abort?
 	this.updateModelMatrix();
 	this.updateDiffuseColor();
 	canvas.nodes[this.name] = this;
 	//}}}
+} //}}}
+Node.prototype.eulerToQuaternion = function(pitch, roll, yaw) { //{{{
+	var t0 = Math.cos(yaw * 0.5);
+	var t1 = Math.sin(yaw * 0.5);
+	var t2 = Math.cos(roll * 0.5);
+	var t3 = Math.sin(roll * 0.5);
+	var t4 = Math.cos(pitch * 0.5);
+	var t5 = Math.sin(pitch * 0.5);
+
+	var w = t0 * t2 * t4 + t1 * t3 * t5;
+	var x = t0 * t3 * t4 - t1 * t2 * t5;
+	var y = t0 * t2 * t5 + t1 * t3 * t4;
+	var z = t1 * t2 * t4 - t0 * t3 * t5;
+	return quat.fromValues(x, y, z, w);
+} //}}}
+Node.prototype.quaternionToEuler = function(q) { //{{{
+	var ysqr = q[1] * q[1];
+
+	// roll (x-axis rotation)
+	var t0 = +2.0 * (q[3] * q[0] + q[1] * q[2]);
+	var t1 = +1.0 - 2.0 * (q[0] * q[0] + ysqr);
+	var roll = Math.atan2(t0, t1);
+
+	// pitch (y-axis rotation)
+	var t2 = +2.0 * (q[3] * q[1] - q[2] * q[0]);
+	t2 = t2 > 1.0 ? 1.0 : t2;
+	t2 = t2 < -1.0 ? -1.0 : t2;
+	var pitch = Math.asin(t2);
+
+	// yaw (z-axis rotation)
+	var t3 = +2.0 * (q[3] * q[2] + q[0] * q[1]);
+	var t4 = +1.0 - 2.0 * (ysqr + q[2] * q[2]);
+	var yaw = Math.atan2(t3, t4);
+	
+	return [pitch * RAD2DEG, roll * RAD2DEG, yaw * RAD2DEG];
 } //}}}
 Node.prototype.updateModelMatrix = function() { //{{{
@@ -83,5 +127,12 @@
 	mat4.rotate(xRotationMatrix, xRotationMatrix, DEG2RAD * this.rotation[0], [1.0, 0.0, 0.0]);
 	mat4.multiply(rotationMatrix, xRotationMatrix, rotationMatrix);
-	mat4.multiply(modelMatrix, rotationMatrix, modelMatrix);	
+	mat4.multiply(modelMatrix, rotationMatrix, modelMatrix);
+
+	//var rotationQuaternionX = this.eulerToQuaternion(0, -DEG2RAD * this.rotation[0], 0);
+	//var rotationQuaternionY = this.eulerToQuaternion(DEG2RAD * this.rotation[1], 0, 0);
+	//mat4.fromQuat(this.rotationMatrix, quat.multiply(quat.create(), rotationQuaternionY, rotationQuaternionX));
+
+	//mat4.multiply(this.modelMatrix, this.rotationMatrix, this.modelMatrix);	
+		
 	
 	mat4.identity(translationMatrix);
@@ -124,5 +175,5 @@
 Node.prototype.patch = function() { //{{{
 	//Emulates the behavior of MATLAB patch function by constructing a mesh from arguments.
-	//Limitations: 
+	//Limitations:
 	//	-Expects pair labeled arguments ('FaceColor','none',...).
 	//	-Only handles Face/Vertices/FaceVertexCData element/node plots.
@@ -149,5 +200,5 @@
 
 	this.mesh = GL.Mesh.load(this.arrays, null, null, this.gl);
-	//this.mesh.computeNormals();
+	this.mesh.computeNormals();
 	this.computeOctree();
 } //}}}
@@ -239,5 +290,5 @@
 	if (this.log !== false && this.log !== 'off') {	
 		caxis = [
-			Math.log10(caxis[0]) / Math.log10(this.log), 
+			Math.log10(caxis[0]) / Math.log10(this.log),
 			Math.log10(caxis[1]) / Math.log10(this.log)
 		];
@@ -389,5 +440,6 @@
 			var j = 0;
 			if (isEmptyOrUndefined(indices)) {
-				for (var key in object) {
+				for (var key in coordinateObject) {
+					console.log(key);
 					array[j++] = coordinateObject[key];
 				}
@@ -407,5 +459,5 @@
 	var meshVertices = mesh.getBuffer('vertices').data;
 	var meshIndicies = mesh.getIndexBuffer('triangles').data;
-	var indicies = new Uint16Array(meshIndicies.length * x.length);
+	var indices = new Uint16Array(meshIndicies.length * x.length);
 	var size = meshVertices.length * x.length / 3;
 	newX = new Float32Array(size);
@@ -423,24 +475,45 @@
 		var offset = i * meshVertices.length / 3;
 		for (var j = 0; j < meshIndicies.length;) {
-			indicies[e++] = meshIndicies[j++] + offset;
-		}
-	}
-	
-	this.patch('Faces', indicies, 'Vertices', [newX, newY, newZ], 'FaceColor', 'interp');
-} //}}}
-Node.prototype.scaleVertices = function(md, x, y, z, scale) { //{{{
+			indices[e++] = meshIndicies[j++] + offset;
+		}
+	}
+	
+	this.patch('Faces', indices, 'Vertices', [newX, newY, newZ], 'FaceColor', 'interp');
+} //}}}
+Node.prototype.scaleVertices = function(md, x, y, z, elements, scale, maskObject) { //{{{
 	//Scales and returns vertices x, y, and z by factor scale. Uses md.geometry.scale for heightscaling in 3d meshes.
 	if (md.mesh.classname() === 'mesh3dsurface') {
-		var xyz, magnitude;
-		x = x.slice();
-		y = y.slice();
-		z = z.slice();
-		for(var i = 0; i < x.length; i++) {
-			xyz = vec3.fromValues(x[i], y[i], z[i]);
-			magnitude = 1 + md.geometry.surface[i] * scale / vec3.length(xyz);
-			vec3.scale(xyz, xyz, magnitude);
-			x[i] = xyz[0];
-			y[i] = xyz[1];
-			z[i] = xyz[2];
+		if (!maskObject.enabled) {
+			var xyz, magnitude;
+			x = x.slice();
+			y = y.slice();
+			z = z.slice();
+			for(var i = 0; i < x.length; i++) {
+				xyz = vec3.fromValues(x[i], y[i], z[i]);
+				magnitude = 1 + md.geometry.surface[i] * scale / vec3.length(xyz);
+				vec3.scale(xyz, xyz, magnitude);
+				x[i] = xyz[0];
+				y[i] = xyz[1];
+				z[i] = xyz[2];
+			}
+		}
+		else {
+			var mask = maskObject.mask;
+			var maskScale = maskObject.scale;
+			var element;
+			for (var i = 0; i < mask.length; i++) {
+				if (mask[i] === 1) {
+					element = elements[i];
+					for (var j = 0; j < 3; j++) {
+						xyz = vec3.fromValues(x[element[j] - 1], y[element[j] - 1], z[element[j] - 1]);
+						magnitude = 1 + md.geometry.surface[element[j] - 1] * scale * maskScale / vec3.length(xyz);
+						vec3.scale(xyz, xyz, magnitude);
+						x[element[j] - 1] = xyz[0];
+						y[element[j] - 1] = xyz[1];
+						z[element[j] - 1] = xyz[2];
+					}
+					
+				}
+			}
 		}
 	}
