Index: issm/trunk-jpl/src/m/plot/webgl.js
===================================================================
--- issm/trunk-jpl/src/m/plot/webgl.js	(revision 21437)
+++ issm/trunk-jpl/src/m/plot/webgl.js	(revision 21641)
@@ -64,12 +64,14 @@
 	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']});
+	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');
@@ -154,10 +156,10 @@
 		alpha:1.0,
 		buffers:[],
-		cullFace:gl.FRONT,
+		cullFace:gl.BACK,
 		disableDepthTest:false, 
 		drawMode:gl.TRIANGLES,
 		drawOrder:0,
 		enabled:true,
-		enableCullFace:false,
+		enableCullFace:true,
 		hideOcean:false,
 		lineWidth:1.0,
@@ -277,5 +279,10 @@
 	}
 } //}}}
-function raycast(canvas, origin, ray) { //{{{
+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; }
@@ -298,44 +305,70 @@
 	ev.preventDefault();
 	if (!(canvas.dataMarkersAllowed && canvas.dataMarkersEnabled)) { return; }
-	updateMarker(canvas, ev.srcEvent.layerX, ev.srcEvent.layerY, true);
-} //}}}
-function updateMarker(canvas, x, y, reset, origin, far) { //{{{
+	initializeMarker(canvas, ev.srcEvent.layerX, ev.srcEvent.layerY, true);
+	brushModify(canvas, ev.srcEvent.layerX, ev.srcEvent.layerY);
+} //}}}
+function brushModify(canvas, x, y) { //{{{
+	//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 = raycast(canvas, x, y);
+
+	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.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 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 hit = raycast(canvas, origin, ray);
-	
+
+	var hit = raycast(canvas, x, y);
+
 	if (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.movieFrame].Thickness;
-			velocity = md.results[canvas.movieFrame].Vel;
-		}
-		else {
-			thickness = md.geometry.thickness;
-			velocity = md.initialization.vel;
-		}
-		
-		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]]];
-		var hitThickness = [thickness[hit.indices[0]], thickness[hit.indices[1]], thickness[hit.indices[2]]];
-		var hitVelocity = [velocity[hit.indices[0]], velocity[hit.indices[1]], velocity[hit.indices[2]]];
-		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;
-		var valueLatitude = Math.abs(hitLatitude[0] * hit.uvw[0] + hitLatitude[1] * hit.uvw[1] + hitLatitude[2] * hit.uvw[2]);
-		var valueLongitude = Math.abs(hitLongitude[0] * hit.uvw[0] + hitLongitude[1] * hit.uvw[1] + hitLongitude[2] * hit.uvw[2]);
-		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];
-		
+		canvas.lastHit = hit;
 		var dataMarkerSize = canvas.dataMarkerSize;
 		if (!canvas.marker) {
@@ -349,9 +382,12 @@
 				'pointer-events': 'all',
 				'cursor': 'pointer',
-				'display': 'none'		
+				'display': 'none'
 			});
 			$('#sim-data-marker-' + canvas.id).tooltipster({
+				contentAsHTML: 'true',
+				animation: 'grow',
 				maxWidth: 320,
-				zIndex: 2200,
+				maxHeight: 320,
+				zIndex: 1000,
 				trigger: 'custom',
 				triggerOpen: {
@@ -364,5 +400,5 @@
 					originClick: true,
 					touchleave: false
-				}
+				},
 			});
 			canvas.marker = $('#sim-data-marker-' + canvas.id);
@@ -374,49 +410,112 @@
 		}
 		
-		
 		canvas.marker.hit = hit;
-		canvas.marker.update = function() {
-			if (!canvas.unitNode) { return; }
-			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;
-			updateMarker(canvas, Math.round(x), Math.round(y), false, origin, far);
-			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'); }
-		};
-		
+
 		if (!canvas.dataMarkerDisplay) {
 			canvas.dataMarkerDisplay = $('#sim-data-marker-' + canvas.id);
 			canvas.dataMarkerDisplay.tooltipster('open');
 		}
-		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); }
+
+		updatePlot(true);
+		repositionMarker();
+		if (reset) { modifyDataMarkersEnabled(true,canvas); }
+	}
+} //}}}
+function updatePlot(reset) {
+	if (!canvas.lastHit) { return; }
+	var hit = canvas.lastHit;
+	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.movieFrame].Thickness;
+		velocity = md.results[canvas.movieFrame].Vel;
+	}
+	else {
+		thickness = md.geometry.thickness;
+		velocity = md.initialization.vel;
+	}
+	
+	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]]];
+	var hitThickness = [thickness[hit.indices[0]], thickness[hit.indices[1]], thickness[hit.indices[2]]];
+	var hitVelocity = [velocity[hit.indices[0]], velocity[hit.indices[1]], velocity[hit.indices[2]]];
+	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;
+	var valueLatitude = Math.abs(hitLatitude[0] * hit.uvw[0] + hitLatitude[1] * hit.uvw[1] + hitLatitude[2] * hit.uvw[2]);
+	var valueLongitude = Math.abs(hitLongitude[0] * hit.uvw[0] + hitLongitude[1] * hit.uvw[1] + hitLongitude[2] * hit.uvw[2]);
+	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]);
+				}
 			}
-			
-			$('#tooltip-content-data-marker-' + canvas.id).html(sprintf.apply(null, format));
-			$('#tooltip-content-data-marker-' + canvas.id).css({
-				'font': canvas.dataMarkerOptions.font
-			});
-		}
-
-		if (reset) { modifyDataMarkersEnabled(true,canvas); }
-	}
-} //}}}
+			else {
+				if (isEmpty || newMovieFrame) {
+					canvas.dataArray.push([canvas.movieFrame,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;
@@ -456,4 +555,5 @@
 function onPinch(ev,canvas,displaylog) { //{{{
 	ev.preventDefault();
+	repositionMarker();
 	if (ev.type == 'pinchstart') { canvas.zoomLast = canvas.zoom; }
 	else { modifyZoom(ev.scale * canvas.zoomLast, canvas, displaylog); }
@@ -461,4 +561,5 @@
 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);
@@ -654,5 +755,5 @@
 	updateCameraMatrix(canvas);
 	
-	if (canvas.marker) { canvas.marker.update(); }
+	//if (canvas.marker) { canvas.marker.update(); }
 	
 	var drawPassNumber = 3;
