Index: issm/trunk/src/m/plot/applyoptions.js
===================================================================
--- issm/trunk/src/m/plot/applyoptions.js	(revision 22758)
+++ issm/trunk/src/m/plot/applyoptions.js	(revision 23189)
@@ -13,5 +13,4 @@
 			//{{{ Variable options initialization
 			var caxis = options.getfieldvalue('caxis');
-			var colorbarinnerlabels = options.getfieldvalue('colorbarinnerlabels','off');
 			var ccanvasid, ctitleid, clabelsid, ccanvas, ctitle, clabels, ccontext, cmap, colorbar, cwidth, cheight, cgradient, color, y, x;
 			//}}}
@@ -49,5 +48,6 @@
 			} //}}}
 			//{{{ Initialize colorbar canvas
-			ccanvasid = options.getfieldvalue('colorbarid', options.getfieldvalue('canvasid').replace('canvas','colorbar-canvas'));
+			let colorbarSlug = options.getfieldvalue('colorbarSlug', options.getfieldvalue('canvasid') + '-colorbar');
+			ccanvasid = colorbarSlug + '-canvas';
 			ccanvas = $('#'+ccanvasid)[0];
 			cwidth = ccanvas.width*options.getfieldvalue('colorbarwidth', 1);
@@ -81,14 +81,6 @@
 			//}}}
 			//{{{ Draw colorbar labels
-			clabelsid = options.getfieldvalue('colorbarid', ccanvasid).replace('canvas','labels');
+			clabelsid = colorbarSlug + '-labels';
 			clabels = $('#'+clabelsid);
-			if (colorbarinnerlabels=='on') {
-				clabels.removeClass('sim-colorbar-labels-outer');
-				clabels.addClass('sim-colorbar-labels-inner');
-			}
-			else {
-				clabels.removeClass('sim-colorbar-labels-inner');
-				clabels.addClass('sim-colorbar-labels-outer');
-			}
 			var clabelstring = '';
 			clabels.empty();
@@ -107,7 +99,7 @@
 			//}}}
 			//{{{ Draw colorbar title
-			ctitleid = options.getfieldvalue('colorbarid', ccanvasid).replace('canvas','heading');
+			ctitleid = colorbarSlug + '-heading';
 			ctitle = $('#'+ctitleid);
-			if (options.exist('colorbartitle')) { ctitle.html(options.getfieldvalue('colorbartitle')); }
+			if (options.exist('colorbarHeader')) { ctitle.html(options.getfieldvalue('colorbarHeader')); }
 			//}}}
 		}
@@ -123,5 +115,5 @@
 	tcontext = tcanvas.getContext('2d');
 	tgradient = tcontext.createLinearGradient(0, 0, 0, 256);
-	
+
 	var cmap = options.getfieldvalue('colormap','jet');
 	var colorbar = colorbars[cmap];
@@ -133,9 +125,9 @@
 	tcontext.fillStyle = tgradient;
 	tcontext.fillRect(0, 0, 256, 256);
-	
+
 	//Allow for special texture colors, drawing each color in equal width vertical rectangles. The last rectanglar section is reserved for the colormap.
 	if (options.exist('maskregion')) {
 		var maskObject = options.getfieldvalue('maskregion',{'enabled':false});
-		if (maskObject.enabled && !VESL.Helpers.isEmptyOrUndefined(maskObject.colors)) {
+		if (maskObject.enabled && !vesl.helpers.isEmptyOrUndefined(maskObject.colors)) {
 			var x = 0;
 			var sections = Object.keys(maskObject.colors).length + 1;
@@ -148,5 +140,5 @@
 		}
 	}
-	
+
 	tURL = tcanvas.toDataURL();
 	if (options.getfieldvalue('clf','on')=='off') {
@@ -157,70 +149,19 @@
 	//}}}
 	//{{{ text display
-	var overlaycanvasid = options.getfieldvalue('overlayid', options.getfieldvalue('canvasid')+'-overlay');
-	var overlaycanvas = $('#'+overlaycanvasid)[0];
-	if (!VESL.Helpers.isEmptyOrUndefined(overlaycanvas)) {
+	var ctx;
+	var overlaycanvasid;
+	var overlaycanvas;
+	//Only intialize overlay canvas once by checking if it's already been defined
+	if (vesl.helpers.isEmptyOrUndefined(canvas.overlaycanvas)) {
 		//Get drawing context and save reference on main WebGL canvas
-		var ctx = overlaycanvas.getContext('2d');
+		overlaycanvasid = options.getfieldvalue('overlayid', options.getfieldvalue('canvasid') + '-overlay')
+		overlaycanvas = $('#' + overlaycanvasid)[0];
+		ctx = overlaycanvas.getContext('2d');
 		canvas.overlaycanvas = overlaycanvas;
-		
-		//Resize interal viewport coordinates to match screenspace coordinates
-		var rect = overlaycanvas.getBoundingClientRect();
-		overlaycanvas.width  = rect.width;
-		overlaycanvas.height = rect.height;
-		
-		//Clear canvas each frame for any new drawings
-		canvas.overlayHandlers['draw'] = function(overlaycanvas) {
-			ctx.clearRect(0, 0, overlaycanvas.width, overlaycanvas.height);
-		}
 	}
-	//{{{ lat long overlay
-	if (options.exist('latlongoverlay')) {
-		var latitudes = {
-			//'-90': 1,
-			//'-65': .999,
-			'-60': 0.994046875,
-			'-45': 0.955729166666666,
-			'-30': 0.9226562500000024,
-			//'-15': 0.830729166666665,
-			'0': 0.74218749999999811,
-			//'15': 0.63932291666666663,
-			'30': 0.523390625000001,
-			'45': 0.4020001,
-			'60': 0.26953124999999978,
-			//'65': 0.225390625,
-			//'90': 0.0,
-		}
-		var longitudes = [-150, -120, -90, -60, -30, 0, 30, 60, 90, 120, 150, 180];
-		canvas.overlayHandlers['latlong'] = function(canvas) {
-			//Transform from world space to viewport space
-			var centerx = overlaycanvas.width / 2;
-			var centery = overlaycanvas.height / 2;
-			var radius = (overlaycanvas.height) / 2;
-
-			//Draw latitudes
-			ctx.setLineDash([5, 10]);
-			for(latitude in latitudes) {
-				ctx.beginPath();
-				ctx.arc(centerx, centery, radius * latitudes[latitude], 0, 2 * Math.PI);
-				ctx.stroke();
-				ctx.font = 'bold ' + String(options.getfieldvalue('colorbarfontsize', 18))+'px "Lato", Helvetica, Arial, sans-serif';
-				ctx.fillStyle = options.getfieldvalue('colorbarfontcolor','black');
-				ctx.strokeStyle = options.getfieldvalue('colorbarfontcolor','black');
-				ctx.textAlign = 'center';
-				ctx.textBaseline = 'middle';
-				ctx.fillText(latitude, centerx, centery + radius * latitudes[latitude]);
-				ctx.strokeText(latitude, centerx, centery + radius * latitudes[latitude]);
-			}
-			//Draw longitudes
-			ctx.setLineDash([1, 0]);
-			for (longitude in longitudes) {
-				ctx.beginPath();
-				ctx.moveTo(centerx, centery);
-				ctx.lineTo(centerx + radius * Math.sin(longitudes[longitude] * DEG2RAD), centery + radius * Math.cos(longitudes[longitude] * DEG2RAD));
-				ctx.stroke();
-			}
-		}
-	} //}}}
-	if (options.exist('textlabels')) {
+	overlaycanvas = canvas.overlaycanvas;
+	ctx = overlaycanvas.getContext('2d');
+
+	if (options.exist('textlabels')) {//{{{
 		//Attatch new overlay handler to display text labels
 		var textLabels = options.getfieldvalue('textlabels',[]);
@@ -236,13 +177,13 @@
 
 				};
-				
+
 				// function declared in slr-gfm sim-front-end-controller.js
 				// if labels are behind the globe sphere then skip iteartion and do not display them
-				if (VESL.UI.isLabelVisible(textLabel)) {
+				if (vesl.ui.isLabelVisible(textLabel)) {
 					//Transform from world space to viewport space
 					var screenPoint = vec3.transformMat4(vec3.create(), textLabel.position, canvas.camera.vpMatrix);
 					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';
@@ -256,5 +197,6 @@
 			}
 		}
-	} //}}}
+	}//}}}
+
 	//{{{ additional rendering nodes
 	if (options.exist('render')) {
@@ -275,4 +217,5 @@
 
 		var renderObjects = options.getfieldvalue('render',{});
+
 		for (var renderObject in renderObjects) {
 			//Modify renderObejct?
@@ -289,6 +232,8 @@
 				color: defaultFor(object.color, 'black'),					//Diffuse color of object
 				height: defaultFor(object.height, 25000),					//Height of object along y axis, currently for clouds only
-				range: defaultFor(object.range, 120000),						//Range of sz plane to spawn object, currently for clouds only
-				quantity: defaultFor(object.quantity, 15)					//Quantity of objects to display, currently for clouds only
+				range: defaultFor(object.range, 120000),					//Range of sz plane to spawn object, currently for clouds only
+				quantity: defaultFor(object.quantity, 15),					//Quantity of objects to display, currently for clouds only
+				source: defaultFor(object.source, 'NY'),					//Quantity of objects to display, currently for clouds only
+				targets: defaultFor(object.targets, ['NY'])					//Quantity of objects to display, currently for clouds only
 			};
 			if (!object.enabled) { continue; }
@@ -367,5 +312,5 @@
 				node.geometryShader('Mesh', mesh, 'Vertices', [object.x, object.y, object.z], 'Indices', object.indices);
 			}
-			if ('axis' === renderObject && !('axis' in canvas.nodes)) {		
+			if ('axis' === renderObject && !('axis' in canvas.nodes)) {
 				node = new Node(
 					'canvas', canvas,
@@ -382,5 +327,5 @@
 				node.patch('Vertices', [object.x, object.y, object.z], 'FaceColor', 'none');
 			}
-			if ('city' === renderObject) {
+			if ('city' === renderObject && !vesl.helpers.isEmptyOrUndefined(overlaycanvas)) {
 				//city
 				var mesh = GL.Mesh.sphere({size: object.size});
@@ -413,10 +358,10 @@
 			}
 			if ('clouds' === renderObject && !('clouds0' in canvas.nodes)) {
-				//clouds				
+				//clouds
 				var mesh = GL.Mesh.fromURL(canvas.assetsPath + '/obj/cloud.obj');
 				for (var i = 0; i < object.quantity; i++) {
 					//TODO: More options, less magic numbers. Add animation. Better shading.
-					var offset = [randomizeAxis(translation[0], object.range), 
-												randomizeCloudHeight(translation[1], object), 
+					var offset = [randomizeAxis(translation[0], object.range),
+												randomizeCloudHeight(translation[1], object),
 												randomizeAxis(translation[2], object.range)];
 					var randomSize = randomizeCloudSize(object.scale);
@@ -436,4 +381,69 @@
 				}
 			}
+			if ('citylines' === renderObject) {
+				//city
+				node = new Node(
+					'canvas', canvas,
+					'options', options,
+					'renderObject', object,
+					'name', 'citylines',
+					'shaderName', 'ColoredDiffuse',
+					'drawMode', gl.LINES,
+					'diffuseColor', object.color,
+					'lineWidth', options.getfieldvalue('linewidth', 1),
+					'scale', [object.scale, object.scale, object.scale],
+					'rotation', [0, 0, 0]
+				);
+
+				//For each target city, calculate the shortest line across the earth by performing a quaternion slerp.
+				//Treat source and target city as vectors to rotate to from the north pole.
+				//Then, slerp between the two rotations, and generate points across equidistance points on the earth to create the line.
+				var north = vec3.fromValues(0, 1, 0);
+				var source = object.source;
+				var sourceXYZ = vec3.fromValues(xcity[source], zcity[source], -ycity[source]);
+				var radius = vec3.length(sourceXYZ);
+				var lineSteps = 50;
+				var lineX = [];
+				var lineY = [];
+				var lineZ = [];
+				var lineXYZ = vec3.create();
+
+				for (var i = 0; i < object.targets.length; i++) {
+					var target = object.targets[i];
+					var targetXYZ = vec3.fromValues(xcity[target], zcity[target], -ycity[target]);
+					var axis = vec3.cross(vec3.create(), sourceXYZ, targetXYZ);
+					vec3.normalize(axis, axis);
+
+					//Get the total angle between the two cities.
+					var sourceXYZAxis = vec3.normalize(vec3.create(), sourceXYZ);
+					var targetXYZAxis = vec3.normalize(vec3.create(), targetXYZ);
+					var dotProduct = vec3.dot(sourceXYZAxis, targetXYZAxis);
+					var totalAngle = Math.acos(dotProduct); //theta = arccos(u . v / (||u|| * ||v||); in this case, ||u|| and ||v|| are 1, since u and v are unit vectors.
+
+					var lineQuat = quat.create();
+					for (var j = 1; j <= lineSteps; j++) {
+						//Calculate the partial rotation to obtain points on the line between the two cities.
+						var angle = j / lineSteps * totalAngle;
+						quat.setAxisAngle(lineQuat, axis, angle);
+						quat.normalize(lineQuat, lineQuat);
+						vec3.transformQuat(lineXYZ, sourceXYZ, lineQuat);
+						//GL.LINES needs 2 points for each line - at the beginning, just use the sourceXYZ.
+						//TODO: Eliminate this if statement.
+						if (j === 1) {
+							lineX.push(sourceXYZ[0]);
+							lineY.push(sourceXYZ[1]);
+							lineZ.push(sourceXYZ[2]);
+						} else {
+							lineX.push(lineX[lineX.length - 1]);
+							lineY.push(lineY[lineY.length - 1]);
+							lineZ.push(lineZ[lineZ.length - 1]);
+						}
+						lineX.push(lineXYZ[0]);
+						lineY.push(lineXYZ[1]);
+						lineZ.push(lineXYZ[2]);
+					}
+				}
+				node.patch('Vertices', [lineX, lineY, lineZ]);
+			}
 		}
 	} //}}}
@@ -445,5 +455,5 @@
 		var minHeigth = object.height - 7000;
 		var randomHeight = (Math.random() * (maxHeight - minHeigth)) + minHeigth;
-		
+
 		return canvasGroundHeight + randomHeight;
 }
@@ -451,5 +461,5 @@
 // assumes that originAxisValue is the mid-value between min and max.
 function randomizeAxis(originAxisValue, range) {
-		return originAxisValue + (Math.random() - 0.5) * (range * 2); 
+		return originAxisValue + (Math.random() - 0.5) * (range * 2);
 }
 
@@ -457,5 +467,5 @@
 	var maxResize = 1.3;
 	var minResize = 0.5;
-	var randomizationFactor = Math.random() * (maxResize - minResize) + minResize; 
+	var randomizationFactor = Math.random() * (maxResize - minResize) + minResize;
 	return scale * randomizationFactor;
 }
