/*This is where we have all our webgl relevant functionality for the plotting routines: */

//{{{ Canvas Initialization
function initCanvas(options) {
	//Initialize open Gl for each canvas, if needed: 
	canvas = document.getElementById(options.getfieldvalue('canvasid'));
	//var canvas = document.getElementById(options.getfieldvalue('canvasid'));
	if (!canvas.initialized) {
		typedArraySliceSupport();
		canvas.gl = initWebGL(canvas,options);
		canvas.nodes = [];
		if (canvas.drawHandler)	{ window.cancelAnimationFrame(canvas.drawHandler); }
		draw(canvas,options);
		canvas.initialized = true;
	}
	return canvas;
}
function initWebGL(canvas,options) { //{{{
	var gl;
	try {
		if (!canvas.gl) {
			gl = GL.create({canvas:canvas});
			// Enable depth testing
			gl.enable(gl.DEPTH_TEST);
			// Near things obscure far things
			gl.depthFunc(gl.LEQUAL);
			// Enable color blending/overlay
			gl.enable(gl.BLEND);
			// Enable face culling
			gl.enable(gl.CULL_FACE); 
			gl.cullFace(gl.FRONT);
			// Load shaders and store them in gl object
			gl.shaders = loadShaders(gl,options.getfieldvalue('rootpath','../../../js/'));
			
			// Add event listeners for canvas
			var displayview = options.getfieldvalue('displayview','off') == 'on';
			var displayzoom = options.getfieldvalue('displayzoom','off') == 'on';
			var mc = new Hammer.Manager(canvas);
			
			mc.add( new Hammer.Tap({ event: 'singletap' }) );
			mc.add(new Hammer.Pan({threshold:0, pointers:0}));
			mc.add(new Hammer.Pinch({threshold:0})).recognizeWith(mc.get('pan'));
			mc.on('singletap', function (ev) {onTap(ev,canvas);});
			mc.on('panstart panmove', function (ev) {onPan(ev,canvas,displayview);});
			mc.on('pinchstart pinchmove', function (ev) {onPinch(ev,canvas,displayview);});
			
			//canvas.addEventListener('mousemove', function (ev) {onTap(ev,canvas);}, false);
			canvas.addEventListener('mousewheel', function (ev) {onZoom(ev,canvas,displayzoom)}, false);
			canvas.addEventListener('DOMMouseScroll', function (ev) {onZoom(ev,canvas,displayzoom)}, false);
		}
		else {
			gl = canvas.gl;
		}
	}
	catch(e) {
		console.log(e);
		return;
	}
	
	// Add context state variables
	//TODO:Group variables in objects for organization and naming
	canvas.gl = gl;
	canvas.rootPath = options.getfieldvalue('rootpath','../../../js/');
	canvas.cameraPosition = vec3.create();
	canvas.cameraMatrix = mat4.create();
	canvas.controlSensitivity = options.getfieldvalue('controlsensitivity',1);
	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.inverseCameraMatrix = mat4.create();
	canvas.id = options.getfieldvalue('canvasid','.sim-canvas');
	canvas.movieFrame = 0;
	canvas.moviePlay = true;
	canvas.movieReverse = false;
	canvas.movieIncrement = true;
	canvas.movieOptions = options.getfieldvalue('movieoptions',{'fps':4,'loop':true});
	canvas.moviefps = options.getfieldvalue('moviefps',5);
	canvas.rotation = options.getfieldvalue('view',[0,90]); //0 azimuth, 90 elevation
	canvas.rotationAzimuthBounds = options.getfieldvalue('azlim',[0,360]);
	canvas.rotationElevationBounds = options.getfieldvalue('ellim',[-180,180]);
	canvas.translation = options.getfieldvalue('origin',[0,0,0]);
	canvas.twod = options.getfieldvalue('2d','off') == 'on';
	canvas.view = options.getfieldvalue('view',[0,90]);
	canvas.viewPanning = options.getfieldvalue('enablepanning','off') == 'on';
	canvas.vInverseMatrix = mat4.create();
	canvas.zoomBounds = options.getfieldvalue('zoomlim',[0.001,100.0]);
	canvas.zoom = clamp(options.getfieldvalue('zoom',1.0), canvas.zoomBounds[0], canvas.zoomBounds[1]);
	canvas.zoomLast = canvas.zoom;
	var backgroundcolor = new RGBColor(options.getfieldvalue('backgroundcolor','lightcyan'));
	if (backgroundcolor.ok) { canvas.backgroundcolor = [backgroundcolor.r/255.0, backgroundcolor.g/255.0, backgroundcolor.b/255.0, 1.0]; }
	else { throw Error(sprintf('s%s%s\n','initWebGL error message: cound not find out background color for curent canvas ',canvas)); }
	
	return gl;
} //}}}
function loadShaders(gl,rootPath) { //{{{
	var shaders = {};
	shaders.Colored = new GL.Shader.fromURL(rootPath+'shaders/Colored.vsh', rootPath+'shaders/Colored.fsh', null, gl);
	shaders.Textured = new GL.Shader.fromURL(rootPath+'shaders/Textured.vsh', rootPath+'shaders/Textured.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);
	return shaders;
} //}}}
function initTexture(gl,imageSource) { //{{{
	return GL.Texture.fromURL(imageSource, {minFilter:gl.LINEAR_MIPMAP_LINEAR, magFilter:gl.LINEAR}, null, gl);
} //}}}
function Node(gl) { //{{{
	//Returns a Node object that contains default display states for webgl object. center represents pivot point of rotation.
	return {
		alpha:1.0,
		buffers:[],
		cullFace:gl.FRONT,
		disableDepthTest:false, 
		drawMode:gl.TRIANGLES,
		drawOrder:0,
		enabled:true,
		enableCullFace:false,
		hideOcean:false,
		lineWidth:1.0,
		maskEnabled:false,
		maskHeight:150.0,
		maskColor:vec4.fromValues(0.0, 0.0, 1.0, 1.0),
		mesh:null,
		name:'node',
		shaderName:'Colored',
		shader:gl.shaders.Colored,
		texture:null,
		useIndexBuffer:true,
		center:vec3.create(), 
		scale:vec3.fromValues(1, 1, 1),
		rotation:vec3.create(),
		translation:vec3.create(),
		modelMatrix:mat4.create(),
		rotationMatrix:mat4.create(),
		inverseModelMatrix:mat4.create(),
		inverseRotationMatrix:mat4.create()
	};
} //}}}
function debugNodes(canvasid) { //{{{
	var canvasid = canvasid || '.sim-canvas';
	var nodes = $(canvasid)[0].nodes;
	console.log(canvasid, 'Nodes:');
	for (var node in nodes) {
		console.log('name: ', nodes[node].name, ' node: ', nodes[node], ' mesh: ', nodes[node].mesh, ' translation: ', nodes[node].translation, ' center:', nodes[node].center, ' rotation:', nodes[node].rotation);
	}
	return nodes;
} //}}}
function updateModelMatrix(node) { //{{{
	var modelMatrix = mat4.create();

	var translationMatrix = mat4.create();
	mat4.translate(translationMatrix, translationMatrix, [-node.center[0],-node.center[1],-node.center[2]]); //scale/rotation centering
	mat4.multiply(modelMatrix, translationMatrix, modelMatrix);
	
	var scaleMatrix = mat4.create();
	mat4.scale(scaleMatrix, scaleMatrix, node.scale);
	mat4.multiply(modelMatrix, scaleMatrix, modelMatrix);
	
	var rotationMatrix = mat4.create();
	var zRotationMatrix = mat4.create();	
	mat4.rotate(zRotationMatrix, zRotationMatrix, DEG2RAD * node.rotation[2], [0.0, 0.0, 1.0]);
	mat4.multiply(rotationMatrix, zRotationMatrix, rotationMatrix);
	var yRotationMatrix = mat4.create();	
	mat4.rotate(yRotationMatrix, yRotationMatrix, DEG2RAD * node.rotation[1], [0.0, 1.0, 0.0]);
	mat4.multiply(rotationMatrix, yRotationMatrix, rotationMatrix);
	var xRotationMatrix = mat4.create();	
	mat4.rotate(xRotationMatrix, xRotationMatrix, DEG2RAD * node.rotation[0], [1.0, 0.0, 0.0]);
	mat4.multiply(rotationMatrix, xRotationMatrix, rotationMatrix);
	mat4.multiply(modelMatrix, rotationMatrix, modelMatrix);	
	
	mat4.identity(translationMatrix);
	mat4.translate(translationMatrix, translationMatrix, node.center); //relative translation
	mat4.multiply(modelMatrix, translationMatrix, modelMatrix);
	
	mat4.identity(translationMatrix);
	mat4.translate(translationMatrix, translationMatrix, node.translation); //absolute translation
	mat4.multiply(modelMatrix, translationMatrix, modelMatrix);
	
	node.modelMatrix = modelMatrix;
	node.inverseModelMatrix = mat4.invert(mat4.create(), modelMatrix);
	node.rotationMatrix = rotationMatrix;
	node.inverseRotationMatrix = mat4.invert(mat4.create(), rotationMatrix);;
} //}}}
function clamp(value, min, max) { //{{{
	return Math.max(min, Math.min(value, max));
} //}}}
function recover(canvasid,name,defaultvalue) { //{{{
	var canvas = document.getElementById(canvasid);
	if (canvas && canvas.hasOwnProperty(name)) { return canvas[name]; }
	return defaultvalue;
} //}}}
function typedArraySliceSupport() { //{{{
	//TypedArray compatibility for Safari/IE
	if (typeof Int8Array !== 'undefined') {
		if (!Int8Array.prototype.fill) { Int8Array.prototype.fill = Array.prototype.fill; }
		if (!Int8Array.prototype.slice) { Int8Array.prototype.slice = Array.prototype.slice; }
	}
	if (typeof Uint8Array !== 'undefined') {
		if (!Uint8Array.prototype.fill) { Uint8Array.prototype.fill = Array.prototype.fill; }
		if (!Uint8Array.prototype.slice) { Uint8Array.prototype.slice = Array.prototype.slice; }
	}
	if (typeof Uint8ClampedArray !== 'undefined') {
		if (!Uint8ClampedArray.prototype.fill) { Uint8ClampedArray.prototype.fill = Array.prototype.fill; }
		if (!Uint8ClampedArray.prototype.slice) { Uint8ClampedArray.prototype.slice = Array.prototype.slice; }
	}
	if (typeof Int16Array !== 'undefined') {
		if (!Int16Array.prototype.fill) { Int16Array.prototype.fill = Array.prototype.fill; }
		if (!Int16Array.prototype.slice) { Int16Array.prototype.slice = Array.prototype.slice; }
	}
	if (typeof Uint16Array !== 'undefined') {
		if (!Uint16Array.prototype.fill) { Uint16Array.prototype.fill = Array.prototype.fill; }
		if (!Uint16Array.prototype.slice) { Uint16Array.prototype.slice = Array.prototype.slice; }
	}
	if (typeof Int32Array !== 'undefined') {
		if (!Int32Array.prototype.fill) { Int32Array.prototype.fill = Array.prototype.fill; }
		if (!Int32Array.prototype.slice) { Int32Array.prototype.slice = Array.prototype.slice; }
	}
	if (typeof Uint32Array !== 'undefined') {
		if (!Uint32Array.prototype.fill) { Uint32Array.prototype.fill = Array.prototype.fill; }
		if (!Uint32Array.prototype.slice) { Uint32Array.prototype.slice = Array.prototype.slice; }
	}
	if (typeof Float32Array !== 'undefined') {
		if (!Float32Array.prototype.fill) { Float32Array.prototype.fill = Array.prototype.fill; }
		if (!Float32Array.prototype.slice) { Float32Array.prototype.slice = Array.prototype.slice; }
	}
	if (typeof Float64Array !== 'undefined') {
		if (!Float64Array.prototype.fill) { Float64Array.prototype.fill = Array.prototype.fill; }
		if (!Float64Array.prototype.slice) { Float64Array.prototype.slice = Array.prototype.slice; }
	}
	if (typeof TypedArray !== 'undefined') {
		if (!TypedArray.prototype.fill) { TypedArray.prototype.fill = Array.prototype.fill; }
		if (!TypedArray.prototype.slice) { TypedArray.prototype.slice = Array.prototype.slice; }
	}
} //}}}
function raycast(canvas, origin, ray) { //{{{
	var mesh = canvas.unitNode.mesh;
	if (!mesh || mesh.ready == false) { return; }
	if (!mesh.octree) { mesh.octree = new GL.Octree(mesh); }
	
	var hit = mesh.octree.testRay(origin, ray, 1e3, 1e10);
	
	if(!hit) { return; }
	
	hit.modelPos = vec3.copy(vec3.create(), hit.pos);
	vec3.transformMat4(hit.pos, hit.pos, canvas.unitNode.modelMatrix);
	vec3.transformMat4(hit.normal, hit.normal, canvas.unitNode.modelMatrix);

	return hit;
} //}}}
//}}}
//{{{ Interface Functions
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.dataMarkersAllowed && canvas.dataMarkersEnabled)) { return; }
	updateMarker(canvas, ev.srcEvent.layerX, ev.srcEvent.layerY, true);
} //}}}
function updateMarker(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);
	
	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];
		
		var dataMarkerSize = canvas.dataMarkerSize;
		if (!canvas.marker) {
			$('#' + canvas.id).after( '<img src=' + canvas.dataMarkerImage + ' alt="data marker" width="' + dataMarkerSize[0] + '" height="' + dataMarkerSize[1] + '" id="sim-data-marker-' + canvas.id + '" class="sim-data-marker noselect tooltip" data-tooltip-content="#tooltip-content-data-marker-' + canvas.id + '"></img><span id="tooltip-content-data-marker-' + canvas.id + '"></span>');
			$('#sim-data-marker-' + canvas.id).css({
				'position': 'absolute', 
				'left': (Math.round(x) - dataMarkerSize[0] / 2) + 'px', 
				'top': (Math.round(y) - dataMarkerSize[1]) + 'px', 
				'width': dataMarkerSize[0] + 'px', 
				'height': dataMarkerSize[1] + 'px',
				'pointer-events': 'all',
				'cursor': 'pointer',
				'display': 'none'		
			});
			$('#sim-data-marker-' + canvas.id).tooltipster({
				maxWidth: 320,
				zIndex: 2200,
				trigger: 'custom',
				triggerOpen: {
					mouseenter: false,
					originClick: true,
					touchstart: false
				},
				triggerClose: {
					mouseleave: false,
					originClick: true,
					touchleave: false
				}
			});
			canvas.marker = $('#sim-data-marker-' + canvas.id);
			canvas.marker.on('click touch', function () {
				canvas.marker.fadeOut(175);
				canvas.dataMarkerDisplay.tooltipster('close');
			});
			canvas.marker.fadeIn(175);
		}
		
		
		canvas.marker.hit = hit;
		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); }
			}
			
			$('#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); }
	}
} //}}}
function onPan(ev,canvas,displaylog) { //{{{
	ev.preventDefault();
	if (ev.type == 'panstart') {
		canvas.lastDeltaX = 0;
		canvas.lastDeltaY = 0;
	}
	if (ev.srcEvent.shiftKey || ev.pointers.length == 2) {
		if (!canvas.viewPanning) return;
		var deltaX = (canvas.lastDeltaX - ev.deltaX) / canvas.clientWidth / canvas.zoom * 2 * canvas.controlSensitivity * 6.371e6;
		var deltaY = (canvas.lastDeltaY - ev.deltaY) / canvas.clientHeight / canvas.zoom * 2 * canvas.controlSensitivity * 6.371e6;
		
		if (canvas.twod) {
			canvas.translation[0] += Math.cos(DEG2RAD * canvas.rotation[0]) * deltaX - Math.sin(DEG2RAD * 0) * deltaY;
			canvas.translation[2] += Math.sin(DEG2RAD * canvas.rotation[0]) * deltaX + Math.cos(DEG2RAD * 0) * deltaY;
		}
		else {
			canvas.translation[0] += Math.cos(DEG2RAD * canvas.rotation[0]) * deltaX - Math.sin(DEG2RAD * canvas.rotation[0]) * deltaY;
			canvas.translation[2] += Math.sin(DEG2RAD * canvas.rotation[0]) * deltaX + Math.cos(DEG2RAD * canvas.rotation[0]) * deltaY;
		}
	}
	else {
		canvas.rotation[0] += (canvas.lastDeltaX - ev.deltaX) / canvas.clientWidth * -2 * canvas.controlSensitivity * RAD2DEG;
		canvas.rotation[1] += (canvas.lastDeltaY - ev.deltaY) / canvas.clientHeight * -2 * canvas.controlSensitivity * RAD2DEG;
		
		if (canvas.rotation[0] > 360) { canvas.rotation[0] -= 360; };
		if (canvas.rotation[0] < -360) { canvas.rotation[0] += 360; };
		if (canvas.rotation[1] > 180) { canvas.rotation[1] -= 360; };
		if (canvas.rotation[1] < -180) { canvas.rotation[1] += 360; };
		
		canvas.rotation[0] = clamp(canvas.rotation[0], canvas.rotationAzimuthBounds[0], canvas.rotationAzimuthBounds[1]);
		canvas.rotation[1] = clamp(canvas.rotation[1], canvas.rotationElevationBounds[0], canvas.rotationElevationBounds[1])
	}
	canvas.lastDeltaX = ev.deltaX;
	canvas.lastDeltaY = ev.deltaY;

	if (displaylog) { console.log(canvas.rotation); }
} //}}}
function onPinch(ev,canvas,displaylog) { //{{{
	ev.preventDefault();
	if (ev.type == 'pinchstart') { canvas.zoomLast = canvas.zoom; }
	else { modifyZoom(ev.scale * canvas.zoomLast, canvas, displaylog); }
} //}}}
function onZoom(ev,canvas,displaylog) { //{{{
	ev.preventDefault();
	var delta = clamp(ev.scale || ev.wheelDelta || -ev.detail, -1, 1) * canvas.controlSensitivity * canvas.zoom / 20;
	modifyZoom(canvas.zoom + delta, canvas, displaylog);
} //}}}
function modifyZoom(value,canvas,displaylog) { //{{{
	canvas.zoom = clamp(value, canvas.zoomBounds[0], canvas.zoomBounds[1]);
	if (displaylog) { console.log(canvas.zoom); }
} //}}}
function modifyDataMarkersEnabled(value,canvas) { //{{{
	canvas.dataMarkersEnabled = value;
	if (!canvas.dataMarkersEnabled && canvas.marker) {
		canvas.marker.fadeOut(175);
		canvas.dataMarkerDisplay.tooltipster('close');
	}
	else if (canvas.dataMarkersEnabled && canvas.marker) {
		canvas.marker.fadeIn(175);
		canvas.dataMarkerDisplay.tooltipster('open');
	}
} //}}}
function toggleMoviePlay(canvas) { //{{{
	canvas.moviePlay = !canvas.moviePlay;
	if (canvas.moviePlay){
		canvas.playButton.find("span").removeClass("fa-play");
		canvas.playButton.find("span").addClass("fa-pause");
	}
	else{
		canvas.playButton.find("span").removeClass("fa-pause");
		canvas.playButton.find("span").addClass("fa-play");
	}
} //}}}
//}}}
//{{{ Drawing Functions
function updateCameraMatrix(canvas) { //{{{
    //Update view matrix and multiply with projection matrix to get the view-projection (camera) matrix.
	var vMatrix = mat4.create();
	var pMatrix = mat4.create();
	var translateMatrix = mat4.create();
	var rotationMatrix = mat4.create();
	var azimuthRotationMatrix = mat4.create();
	var elevationRotationMatrix = mat4.create();
	var aspectRatio = canvas.clientWidth / canvas.clientHeight;
	var cameraPosition = vec3.create();

	if (canvas.twod) { mat4.ortho(pMatrix, -aspectRatio*6.371e6/canvas.zoom, aspectRatio*6.371e6/canvas.zoom, -6.371e6/canvas.zoom, 6.371e6/canvas.zoom, -1.0, 1e10); }
	else { mat4.perspective(pMatrix, 45 * DEG2RAD, aspectRatio, 1e3, 1e10); }
	
	//Apply worldspace translation
	mat4.translate(translateMatrix, translateMatrix, [-canvas.translation[0],-canvas.translation[1],-canvas.translation[2]]);
	mat4.multiply(vMatrix, translateMatrix, vMatrix);
	
	//Calculate rotation around camera focal point about worldspace origin
	if (canvas.twod) {
		mat4.rotate(azimuthRotationMatrix, azimuthRotationMatrix, DEG2RAD * 0, [0, 1, 0]);
		mat4.rotate(elevationRotationMatrix, elevationRotationMatrix, DEG2RAD * 90, [1, 0, 0]);
		mat4.multiply(rotationMatrix, elevationRotationMatrix, azimuthRotationMatrix);
	}
	else {
		mat4.rotate(azimuthRotationMatrix, azimuthRotationMatrix, DEG2RAD * canvas.rotation[0], [0, 1, 0]);
		mat4.rotate(elevationRotationMatrix, elevationRotationMatrix, DEG2RAD * canvas.rotation[1], [1, 0, 0]);
		mat4.multiply(rotationMatrix, elevationRotationMatrix, azimuthRotationMatrix);
	}
	
	//Apply rotation transform
	mat4.multiply(vMatrix, rotationMatrix, vMatrix);

	//Apply screenspace translation
	mat4.identity(translateMatrix);
	mat4.translate(translateMatrix, translateMatrix, [0.0, 0.0, -6.371e6/canvas.zoom]);
	mat4.multiply(vMatrix, translateMatrix, vMatrix);
	
	//Calculate fields for lighting and raycasts
	mat4.invert(canvas.vInverseMatrix, vMatrix);
	
	//Apply projection matrix to get camera matrix
	mat4.multiply(canvas.cameraMatrix, pMatrix, vMatrix);
	mat4.invert(canvas.inverseCameraMatrix, canvas.cameraMatrix);
	vec3.transformMat4(canvas.cameraPosition, cameraPosition, canvas.inverseCameraMatrix);
}//}}}
function drawSceneGraphNode(canvas,node) { //{{{
	if (!node.enabled) { return; }

	var gl = canvas.gl;
	gl.makeCurrent();
	
	var mvpMatrix = mat4.create();
	mat4.multiply(mvpMatrix, canvas.cameraMatrix, node.modelMatrix);
	
	if (node.texture) { node.texture.bind(0); }
	if (node.disableDepthTest) { gl.disable(gl.DEPTH_TEST); }
	if (node.enableCullFace) { gl.enable(gl.CULL_FACE); }

	gl.cullFace(node.cullFace);
	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.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;
	
	node.shader.uniforms({
		m4MVP: mvpMatrix,
		m4Model: node.modelMatrix,
		u_texture: 0,
		u_alpha: node.alpha,
		u_maskEnabled: node.maskEnabled,
		u_maskHeight: node.maskHeight,
		u_maskColor: node.maskColor,
		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
	}).draw(node.mesh, node.drawMode, 'triangles');
	
	gl.enable(gl.DEPTH_TEST);
	gl.disable(gl.CULL_FACE);
} //}}}
function draw(canvas,options) { //{{{
	if (canvas.textcanvas) { canvas.textcanvas.draw(canvas); }

	var nodes = canvas.nodes;
	if (nodes.length < 1) {
		canvas.drawHandler = window.requestAnimationFrame(function(time) { draw(canvas,options); });
		return;
	}
	for (var node in nodes) {	
		if ((nodes[node].texture && nodes[node].texture.ready == false) || nodes[node].shader.ready == false || nodes[node].mesh.ready == false) {
			canvas.drawHandler = window.requestAnimationFrame(function(time) { draw(canvas,options); });
			return;
		}
	}
	
	var rect = canvas.getBoundingClientRect();
	canvas.width  = rect.width;
	canvas.height = rect.height;
	
	var gl = canvas.gl;
	gl.makeCurrent(); //litegl function to handle switching between multiple canvases
	gl.viewport(0, 0, rect.width, rect.height);
	gl.clearColor(canvas.backgroundcolor[0], canvas.backgroundcolor[1], canvas.backgroundcolor[2], canvas.backgroundcolor[3]);
	gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
	
	updateCameraMatrix(canvas);
	
	if (canvas.marker) { canvas.marker.update(); }
	
	var drawPassNumber = 3;
	for (var i = drawPassNumber - 1; i >= 0; i--) {
		for (var node in nodes) {
			if (nodes[node].drawOrder == i) { drawSceneGraphNode(canvas,nodes[node]); }
		}
	}

	canvas.drawHandler = window.requestAnimationFrame(function(time) { draw(canvas,options); });
} //}}}
//}}}
