<commit_before>/*
 * __author__ = "Meg Pirrung"
 * __copyright__ = "Copyright 2014, multivis"
 * __credits__ = ["Meg Pirrung"]
 * __license__ = "MIT"
 * __adaptation__ = "EMPeror by Meg Pirrung, Yoshiki Vazquez Baeza and
                  Antonio Gonzalez Pena. EMPeror BSD 2013"
 */

function ThreeD() {
	/*global vars*/
	var data;
	var rainbow = new Rainbow();
	var color;
	var groups = [];
	var g_plotSpheres = {};
	var g_segments = 8, g_rings = 8, g_radius = 1;
	var g_xAxisLength;
	var g_yAxisLength;
	var g_zAxisLength;
	var g_xMaximumValue;
	var g_yMaximumValue;
	var g_zMaximumValue;
	var g_xMinimumValue;
	var g_yMinimumValue;
	var g_zMinimumValue;
	var g_maximum;
	var g_pc1Label;
	var g_pc2Label;
	var g_pc3Label;
	var g_number_of_custom_axes = 0;
	var xAxisIndex = 1;
	var yAxisIndex = 2;
	var zAxisIndex = 3;

	// sample identifiers of all items that are plotted
	var g_plotIds = [];

	// line objects used to represent the axes
	var g_xAxisLine;
	var g_yAxisLine;
	var g_zAxisLine;

	// general multipurpose variables
	var g_elementsGroup; // group that holds the plotted shapes
	var g_spheres = []; // group that holds spheres
	var g_genericSphere; // generic sphere used for plots
	var g_categoryName = ""; // current coloring category
	var g_sphereScaler = 1.0;
	var g_sceneCamera;

	this.setData = function(root) {
		data = root;

		//get all the axes
		var axes = d3.keys(data[0])
		//remove individual as an axis
		axes.splice(axes.indexOf('Individual'),1)

		data.forEach(function(sample){
			groups.push(sample.Individual.substring(0,11))
			sample.Individual = sample.Individual.substring(0,11)
			var temp = {}
			axes.forEach(function(d) {
				sample[d] = +sample[d]
			});
		});
		groups = dedupe(groups)
		rainbow.setSpectrum('green','blue','red','yellow')
		rainbow.setNumberRange(0,groups.length);

		setMinMaxVals();

		var plot = $('#plot');
		var particles, geometry, parameters, i, h, color;
		var mouseX = 0, mouseY = 0;

		var windowWidth = Math.min(document.getElementById('visWrapper').offsetWidth,document.getElementById('visWrapper').offsetHeight), view_angle = 35, view_near = 0.0000001, view_far = 10000;
		var winAspect = document.getElementById('visWrapper').offsetWidth/document.getElementById('visWrapper').offsetHeight;

		// Detecting that webgl is activated
		if ( !window.WebGLRenderingContext ) {
			window.location = "http://get.webgl.org";
		} else {
			init();
			animate();
		}

		function init() {
			d3.select("#visWrapper").append("div")
			  .attr("id", "axislabels")
			  .attr("class", "axislabels");

			// assign a position to the camera befor associating it with other
			// objects, else the original position will be lost and not make sense
			g_sceneCamera = new THREE.PerspectiveCamera(view_angle, winAspect, view_near, view_far);
			g_sceneCamera.position.set(0, 0, 0);

			$('#plot canvas').attr('width',document.getElementById('visWrapper').offsetWidth);
			$('#plot canvas').attr('height',document.getElementById('visWrapper').offsetHeight);
			// $('#plot canvas').attr('width',500);
			// $('#plot canvas').attr('height',500);

			g_mainScene = new THREE.Scene();
			g_mainScene.add(g_sceneCamera);

			g_genericSphere = new THREE.SphereGeometry(g_radius, g_segments, g_rings);
			g_elementsGroup = new THREE.Object3D();
			g_mainScene.add(g_elementsGroup);

			drawSpheres();

			// the light is attached to the camera to provide a 3d perspective
			g_sceneLight = new THREE.DirectionalLight(0x999999, 2);
			g_sceneLight.position.set(1,1,1).normalize();
			g_sceneCamera.add(g_sceneLight);

			// Adding camera
			g_sceneControl = new THREE.TrackballControls(g_sceneCamera, document.getElementById('plot'));
			g_sceneControl.rotateSpeed = 1.0;
			g_sceneControl.zoomSpeed = 1.2;
			g_sceneControl.panSpeed = 0.8;
			g_sceneControl.noZoom = false;
			g_sceneControl.noPan = false;
			g_sceneControl.staticMoving = true;
			g_sceneControl.dynamicDampingFactor = 0.3;
			g_sceneControl.keys = [ 65, 83, 68 ];

			// white is the default background color for the scene
			var rendererBackgroundColor = new THREE.Color();
			rendererBackgroundColor.setHex("0xffffff");

			// renderer, the default background color is white
			g_mainRenderer = new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true });

			g_mainRenderer.setClearColor(rendererBackgroundColor, 1);
			g_mainRenderer.setSize( document.getElementById('visWrapper').offsetWidth, document.getElementById('visWrapper').offsetHeight );
			g_mainRenderer.sortObjects = true;
			plot.append(g_mainRenderer.domElement);

			colorSpheres();
			drawAxisLines();
			buildAxisLabels();
			resetCamera();
		}

		function animate() {
			requestAnimationFrame( animate );
			render();

			var labelCoordinates;
			// reposition the labels for the axes in the 3D plot
			labelCoordinates = toScreenXY(new THREE.Vector3(g_xMaximumValue, g_yMinimumValue, g_zMinimumValue), g_sceneCamera, $('#plot'));
			$("#pc1_label").css('left', labelCoordinates['x'])
			$("#pc1_label").css('top', labelCoordinates['y'])
			labelCoordinates = toScreenXY(new THREE.Vector3(g_xMinimumValue, g_yMaximumValue, g_zMinimumValue), g_sceneCamera, $('#plot'));
			$("#pc2_label").css('left', labelCoordinates['x'])
			$("#pc2_label").css('top', labelCoordinates['y'])
			labelCoordinates = toScreenXY(new THREE.Vector3(g_xMinimumValue, g_yMinimumValue, g_zMaximumValue), g_sceneCamera, $('#plot'));
			$("#pc3_label").css('left', labelCoordinates['x'])
			$("#pc3_label").css('top', labelCoordinates['y'])
		}

		function render() {
			g_sceneControl.update();
			// g_mainRenderer.setSize( document.getElementById('visWrapper').offsetWidth, document.getElementById('visWrapper').offsetHeight );
			g_mainRenderer.render( g_mainScene, g_sceneCamera );
		}

		/*This function recenter the camera to the initial position it had*/
		function resetCamera() {
			// We need to reset the camera controls first before modifying the values of the camera (this is the reset view!)
			g_sceneControl.reset();

			g_sceneCamera.aspect = 1;
			g_sceneCamera.rotation.set( 0, 0, 0 );
			g_sceneCamera.updateProjectionMatrix();
		}

		function drawSpheres() {
			data.forEach(function(d) {
				//draw ball
				var mesh = new THREE.Mesh( g_genericSphere, new THREE.MeshLambertMaterial() );
				mesh.material.color = new THREE.Color()
				mesh.material.transparent = true;
				mesh.material.depthWrite = false;
				mesh.material.opacity = 1;
				mesh.position.set(d[xAxisIndex], d[yAxisIndex], d[zAxisIndex]);
				mesh.updateMatrix();
				mesh.matrixAutoUpdate = true;
				g_mainScene.add( mesh );
				var obj = { 'mesh': mesh, 'obj': d };
				g_spheres.push( obj );
			});
		}

		function colorSpheres() {
			g_spheres.forEach(function(d){
				d.mesh.material.color.setHex('0x'+rainbow.colorAt(groups.indexOf(d.obj.Individual)))
			})
		}

		function setMinMaxVals() {
			g_xMaximumValue = d3.max(data, function(d){
				return d[xAxisIndex]
			});
			g_yMaximumValue = d3.max(data, function(d){
				return d[yAxisIndex]
			});
			g_zMaximumValue = d3.max(data, function(d){
				return d[zAxisIndex]
			});
			g_xMinimumValue = d3.min(data, function(d){
				return d[xAxisIndex]
			});
			g_yMinimumValue = d3.min(data, function(d){
				return d[yAxisIndex]
			});
			g_zMinimumValue = d3.min(data, function(d){
				return d[zAxisIndex]
			});

			g_xAxisLength = g_xMaximumValue - g_xMinimumValue;
			g_yAxisLength = g_yMaximumValue - g_yMinimumValue;
			g_zAxisLength = g_zMaximumValue - g_zMinimumValue;

			g_maximum = d3.max([g_xMaximumValue,g_yMaximumValue,g_zMaximumValue]);
		}

		function drawAxisLines() {
			// removing axes, if they do not exist the scene doesn't complain
			g_mainScene.remove(g_xAxisLine);
			g_mainScene.remove(g_yAxisLine);
			g_mainScene.remove(g_zAxisLine);

			// one line for each of the axes
			g_xAxisLine = makeLine([g_xMinimumValue, g_yMinimumValue, g_zMinimumValue],
				[g_xMaximumValue, g_yMinimumValue, g_zMinimumValue], '0x000000', 3);
			g_yAxisLine = makeLine([g_xMinimumValue, g_yMinimumValue, g_zMinimumValue],
				[g_xMinimumValue, g_yMaximumValue, g_zMinimumValue], '0x000000', 3);
			g_zAxisLine = makeLine([g_xMinimumValue, g_yMinimumValue, g_zMinimumValue],
				[g_xMinimumValue, g_yMinimumValue, g_zMaximumValue], '0x000000', 3);

			// axes shouldn't be transparent
			g_xAxisLine.material.transparent = false;
			g_yAxisLine.material.transparent = false;
			g_zAxisLine.material.transparent = false;

			g_mainScene.add(g_xAxisLine)
			g_mainScene.add(g_yAxisLine)
			g_mainScene.add(g_zAxisLine)
		}

		/*Utility function to draw two-vertices lines at a time

		  This function allows you to create a line with only two vertices i. e. the
		  start point and the end point, plus the color and width of the line. The
		  start and end point must be 3 elements array. The color must be a hex-string
		  or a hex number.
		*/
		function makeLine(coords_a, coords_b, color, width){
			// based on the example described in:
			// https://github.com/mrdoob/three.js/wiki/Drawing-lines
			var material, geometry, line;

			// make the material transparent and with full opacity
			material = new THREE.LineBasicMaterial({color:color, linewidth:width});
			material.matrixAutoUpdate = true;
			material.transparent = true;
			material.opacity = 1.0;

			// add the two vertices to the geometry
			geometry = new THREE.Geometry();
			geometry.vertices.push(new THREE.Vector3(coords_a[0], coords_a[1], coords_a[2]));
			geometry.vertices.push(new THREE.Vector3(coords_b[0], coords_b[1], coords_b[2]));

			// the line will contain the two vertices and the described material
			line = new THREE.Line(geometry, material);

			return line;
		}

		/*Builds the axes labels from ground up after changing the axes*/
		function buildAxisLabels() {
			g_pc1Label = "Axis "+xAxisIndex
			g_pc2Label = "Axis "+yAxisIndex
			g_pc3Label = "Axis "+zAxisIndex

			//build axis labels
			var axislabelhtml = "";
			var xcoords = toScreenXY(new THREE.Vector3(g_xMaximumValue, g_yMinimumValue, g_zMinimumValue),g_sceneCamera,plot);
			axislabelhtml += "";
			axislabelhtml += g_pc1Label;
			axislabelhtml += "";
			var ycoords = toScreenXY(new THREE.Vector3(g_xMinimumValue, g_yMaximumValue, g_zMinimumValue),g_sceneCamera,plot);
			axislabelhtml += "";
			axislabelhtml += g_pc2Label;
			axislabelhtml += "";
			var zcoords = toScreenXY(new THREE.Vector3(g_xMinimumValue, g_yMinimumValue, g_zMaximumValue),g_sceneCamera,plot);
			axislabelhtml += "";
			axislabelhtml += g_pc3Label;
			axislabelhtml += "";
			document.getElementById("axislabels").innerHTML = axislabelhtml;
		}

		/*This function finds the screen coordinates of any position in the current plot.

		  The main purpose of this function is to be used for calculating the placement
		  of the labels.
		*/
		function toScreenXY( position, camera, jqdiv ) {

			var screenPosition = position.clone();
			var screenProjectionMatrix = new THREE.Matrix4();

			// multiply the matrices and aply the vector to the projection matrix
			screenProjectionMatrix.multiplyMatrices( camera.projectionMatrix,
				camera.matrixWorldInverse);
			screenPosition.applyProjection(screenProjectionMatrix);
			return { x: (screenPosition.x + 1)*jqdiv.width()/2 + jqdiv.offset().left,
				y: (-screenPosition.y+1)*jqdiv.height()/2 + jqdiv.offset().top};
		}

		// Taken from http://stackoverflow.com/questions/18082/validate-numbers-in-javascript-isnumeric
		function isNumeric(n) {
		  return !isNaN(parseFloat(n)) && isFinite(n);
		}

	  	function dedupe(list) {
	  	   var set = {};
	  	   for (var i = 0; i < list.length; i++)
	  	      set[list[i]] = true;
	  	   list = [];
	  	   for (var v in set)
	  	      list.push(v);
	  	   return list;
	  	}
	}
}<commit_msg>checking in latest fixes for 3d view<commit_after>/*
 * __author__ = "Meg Pirrung"
 * __copyright__ = "Copyright 2014, multivis"
 * __credits__ = ["Meg Pirrung"]
 * __license__ = "MIT"
 * __adaptation__ = "EMPeror by Meg Pirrung, Yoshiki Vazquez Baeza and
                  Antonio Gonzalez Pena. EMPeror BSD 2013"
 */

function ThreeD() {
	/*global vars*/
	var data;
	var rainbow = new Rainbow();
	var color;
	var groups = [];
	var g_plotSpheres = {};
	var g_segments = 8, g_rings = 8, g_radius = .02;
	var g_xAxisLength;
	var g_yAxisLength;
	var g_zAxisLength;
	var g_xMaximumValue;
	var g_yMaximumValue;
	var g_zMaximumValue;
	var g_xMinimumValue;
	var g_yMinimumValue;
	var g_zMinimumValue;
	var g_maximum;
	var g_pc1Label;
	var g_pc2Label;
	var g_pc3Label;
	var g_number_of_custom_axes = 0;
	var xAxisIndex = 1;
	var yAxisIndex = 2;
	var zAxisIndex = 3;

	// sample identifiers of all items that are plotted
	var g_plotIds = [];

	// line objects used to represent the axes
	var g_xAxisLine;
	var g_yAxisLine;
	var g_zAxisLine;

	// general multipurpose variables
	var g_elementsGroup; // group that holds the plotted shapes
	var g_spheres = []; // group that holds spheres
	var g_genericSphere; // generic sphere used for plots
	var g_categoryName = ""; // current coloring category
	var g_sphereScaler = 1.0;
	var g_sceneCamera;

	this.setData = function(root) {
		data = root;

		//get all the axes
		var axes = d3.keys(data[0])
		//remove individual as an axis
		axes.splice(axes.indexOf('Individual'),1)

		data.forEach(function(sample){
			groups.push(sample.Individual.substring(0,11))
			sample.Individual = sample.Individual.substring(0,11)
			var temp = {}
			axes.forEach(function(d) {
				sample[d] = +sample[d]
			});
		});
		groups = dedupe(groups)
		rainbow.setSpectrum('green','blue','red','yellow')
		rainbow.setNumberRange(0,groups.length);

		setMinMaxVals();

		var plot = $('#plot');
		var particles, geometry, parameters, i, h, color;
		var mouseX = 0, mouseY = 0;

		var windowWidth = Math.min(document.getElementById('visWrapper').offsetWidth,document.getElementById('visWrapper').offsetHeight), view_angle = 40, view_near = 1, view_far = 10000;
		var winAspect = document.getElementById('visWrapper').offsetWidth/document.getElementById('visWrapper').offsetHeight;

		// Detecting that webgl is activated
		if ( !window.WebGLRenderingContext ) {
			window.location = "http://get.webgl.org";
		} else {
			init();
			animate();
		}

		function init() {
			d3.select("#visWrapper").append("div")
			  .attr("id", "axislabels")
			  .attr("class", "axislabels");

			// assign a position to the camera befor associating it with other
			// objects, else the original position will be lost and not make sense
			g_sceneCamera = new THREE.PerspectiveCamera(view_angle, winAspect, view_near, view_far);

			g_sceneCamera.aspect = document.getElementById('visWrapper').offsetWidth/document.getElementById('visWrapper').offsetHeight;
			g_sceneCamera.rotation.set( 0, 0, 0 );
			g_sceneCamera.updateProjectionMatrix();
			g_sceneCamera.position.set(0 , 0, (g_maximum*4.8) + g_radius);

			$('#plot canvas').attr('width',document.getElementById('visWrapper').offsetWidth);
			$('#plot canvas').attr('height',document.getElementById('visWrapper').offsetHeight);
			// $('#plot canvas').attr('width',500);
			// $('#plot canvas').attr('height',500);

			g_mainScene = new THREE.Scene();
			g_mainScene.add(g_sceneCamera);

			g_genericSphere = new THREE.SphereGeometry(g_radius, g_segments, g_rings);
			g_elementsGroup = new THREE.Object3D();
			g_mainScene.add(g_elementsGroup);

			drawSpheres();

			// the light is attached to the camera to provide a 3d perspective
			g_sceneLight = new THREE.DirectionalLight(0x999999, 2);
			g_sceneLight.position.set(1,1,1).normalize();
			g_sceneCamera.add(g_sceneLight);

			// Adding camera
			g_sceneControl = new THREE.TrackballControls(g_sceneCamera, document.getElementById('plot'));
			g_sceneControl.rotateSpeed = 1.0;
			g_sceneControl.zoomSpeed = 1.2;
			g_sceneControl.panSpeed = 0.8;
			g_sceneControl.noZoom = false;
			g_sceneControl.noPan = false;
			g_sceneControl.staticMoving = true;
			g_sceneControl.dynamicDampingFactor = 0.3;
			g_sceneControl.keys = [ 65, 83, 68 ];

			// white is the default background color for the scene
			var rendererBackgroundColor = new THREE.Color();
			rendererBackgroundColor.setHex("0xffffff");

			// renderer, the default background color is white
			g_mainRenderer = new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true });

			g_mainRenderer.setClearColor(rendererBackgroundColor, 1);
			g_mainRenderer.setSize( document.getElementById('visWrapper').offsetWidth, document.getElementById('visWrapper').offsetHeight );
			g_mainRenderer.sortObjects = true;
			plot.append(g_mainRenderer.domElement);

			colorSpheres();
			drawAxisLines();
			buildAxisLabels();
			resetCamera();
		}

		function animate() {
			requestAnimationFrame( animate );
			render();

			var labelCoordinates;
			// reposition the labels for the axes in the 3D plot
			labelCoordinates = toScreenXY(new THREE.Vector3(g_xMaximumValue, g_yMinimumValue, g_zMinimumValue), g_sceneCamera, $('#plot'));
			$("#pc1_label").css('left', labelCoordinates['x'])
			$("#pc1_label").css('top', labelCoordinates['y'])
			labelCoordinates = toScreenXY(new THREE.Vector3(g_xMinimumValue, g_yMaximumValue, g_zMinimumValue), g_sceneCamera, $('#plot'));
			$("#pc2_label").css('left', labelCoordinates['x'])
			$("#pc2_label").css('top', labelCoordinates['y'])
			labelCoordinates = toScreenXY(new THREE.Vector3(g_xMinimumValue, g_yMinimumValue, g_zMaximumValue), g_sceneCamera, $('#plot'));
			$("#pc3_label").css('left', labelCoordinates['x'])
			$("#pc3_label").css('top', labelCoordinates['y'])
		}

		function render() {
			g_sceneControl.update();
			// g_mainRenderer.setSize( document.getElementById('visWrapper').offsetWidth, document.getElementById('visWrapper').offsetHeight );
			g_mainRenderer.render( g_mainScene, g_sceneCamera );
		}

		/*This function recenter the camera to the initial position it had*/
		function resetCamera() {
			// We need to reset the camera controls first before modifying the values of the camera (this is the reset view!)
			// g_sceneControl.reset();

			// g_sceneCamera.aspect = 1;
			// g_sceneCamera.rotation.set( 0, 0, 0 );
			// g_sceneCamera.updateProjectionMatrix();
		}

		function drawSpheres() {
			data.forEach(function(d) {
				//draw ball
				var mesh = new THREE.Mesh( g_genericSphere, new THREE.MeshLambertMaterial() );
				mesh.material.color = new THREE.Color()
				mesh.material.transparent = true;
				mesh.material.depthWrite = false;
				mesh.material.opacity = 1;
				mesh.position.set(d[xAxisIndex], d[yAxisIndex], d[zAxisIndex]);
				mesh.updateMatrix();
				mesh.matrixAutoUpdate = true;
				g_mainScene.add( mesh );
				var obj = { 'mesh': mesh, 'obj': d };
				g_spheres.push( obj );
			});
		}

		function colorSpheres() {
			g_spheres.forEach(function(d){
				d.mesh.material.color.setHex('0x'+rainbow.colorAt(groups.indexOf(d.obj.Individual)))
			})
		}

		function setMinMaxVals() {
			g_xMaximumValue = d3.max(data, function(d){
				return d[xAxisIndex]
			});
			g_yMaximumValue = d3.max(data, function(d){
				return d[yAxisIndex]
			});
			g_zMaximumValue = d3.max(data, function(d){
				return d[zAxisIndex]
			});
			g_xMinimumValue = d3.min(data, function(d){
				return d[xAxisIndex]
			});
			g_yMinimumValue = d3.min(data, function(d){
				return d[yAxisIndex]
			});
			g_zMinimumValue = d3.min(data, function(d){
				return d[zAxisIndex]
			});

			g_xAxisLength = g_xMaximumValue - g_xMinimumValue;
			g_yAxisLength = g_yMaximumValue - g_yMinimumValue;
			g_zAxisLength = g_zMaximumValue - g_zMinimumValue;

			g_maximum = d3.max([g_xMaximumValue,g_yMaximumValue,g_zMaximumValue]);
		}

		function drawAxisLines() {
			// removing axes, if they do not exist the scene doesn't complain
			// g_mainScene.remove(g_xAxisLine);
			// g_mainScene.remove(g_yAxisLine);
			// g_mainScene.remove(g_zAxisLine);

			// one line for each of the axes
			g_xAxisLine = makeLine([g_xMinimumValue, g_yMinimumValue, g_zMinimumValue],
				[g_xMaximumValue, g_yMinimumValue, g_zMinimumValue], '0x000000', 3);
			g_yAxisLine = makeLine([g_xMinimumValue, g_yMinimumValue, g_zMinimumValue],
				[g_xMinimumValue, g_yMaximumValue, g_zMinimumValue], '0x000000', 3);
			g_zAxisLine = makeLine([g_xMinimumValue, g_yMinimumValue, g_zMinimumValue],
				[g_xMinimumValue, g_yMinimumValue, g_zMaximumValue], '0x000000', 3);

			// axes shouldn't be transparent
			g_xAxisLine.material.transparent = false;
			g_yAxisLine.material.transparent = false;
			g_zAxisLine.material.transparent = false;

			g_mainScene.add(g_xAxisLine)
			g_mainScene.add(g_yAxisLine)
			g_mainScene.add(g_zAxisLine)
		}

		/*Utility function to draw two-vertices lines at a time

		  This function allows you to create a line with only two vertices i. e. the
		  start point and the end point, plus the color and width of the line. The
		  start and end point must be 3 elements array. The color must be a hex-string
		  or a hex number.
		*/
		function makeLine(coords_a, coords_b, color, width){
			// based on the example described in:
			// https://github.com/mrdoob/three.js/wiki/Drawing-lines
			var material, geometry, line;

			// make the material transparent and with full opacity
			material = new THREE.LineBasicMaterial({color:color, linewidth:width});
			material.matrixAutoUpdate = true;
			material.transparent = true;
			material.opacity = 1.0;

			// add the two vertices to the geometry
			geometry = new THREE.Geometry();
			geometry.vertices.push(new THREE.Vector3(coords_a[0], coords_a[1], coords_a[2]));
			geometry.vertices.push(new THREE.Vector3(coords_b[0], coords_b[1], coords_b[2]));

			// the line will contain the two vertices and the described material
			line = new THREE.Line(geometry, material);

			return line;
		}

		/*Builds the axes labels from ground up after changing the axes*/
		function buildAxisLabels() {
			g_pc1Label = "Axis "+xAxisIndex
			g_pc2Label = "Axis "+yAxisIndex
			g_pc3Label = "Axis "+zAxisIndex

			//build axis labels
			var axislabelhtml = "";
			var xcoords = toScreenXY(new THREE.Vector3(g_xMaximumValue, g_yMinimumValue, g_zMinimumValue),g_sceneCamera,$('#plot'));
			axislabelhtml += "";
			axislabelhtml += g_pc1Label;
			axislabelhtml += "";
			var ycoords = toScreenXY(new THREE.Vector3(g_xMinimumValue, g_yMaximumValue, g_zMinimumValue),g_sceneCamera,$('#plot'));
			axislabelhtml += "";
			axislabelhtml += g_pc2Label;
			axislabelhtml += "";
			var zcoords = toScreenXY(new THREE.Vector3(g_xMinimumValue, g_yMinimumValue, g_zMaximumValue),g_sceneCamera,$('#plot'));
			axislabelhtml += "";
			axislabelhtml += g_pc3Label;
			axislabelhtml += "";
			document.getElementById("axislabels").innerHTML = axislabelhtml;
		}

		/*This function finds the screen coordinates of any position in the current plot.

		  The main purpose of this function is to be used for calculating the placement
		  of the labels.
		*/
		function toScreenXY( position, camera, jqdiv ) {

			var screenPosition = position.clone();
			var screenProjectionMatrix = new THREE.Matrix4();

			// multiply the matrices and aply the vector to the projection matrix
			screenProjectionMatrix.multiplyMatrices( camera.projectionMatrix,
				camera.matrixWorldInverse );
			screenPosition.applyProjection(screenProjectionMatrix);
			return { x: (screenPosition.x + 1)*jqdiv.width()/2 + jqdiv.offset().left,
				y: (-screenPosition.y+1)*jqdiv.height()/2 + jqdiv.offset().top};
		}

		// Taken from http://stackoverflow.com/questions/18082/validate-numbers-in-javascript-isnumeric
		function isNumeric(n) {
		  return !isNaN(parseFloat(n)) && isFinite(n);
		}

	  	function dedupe(list) {
	  	   var set = {};
	  	   for (var i = 0; i < list.length; i++)
	  	      set[list[i]] = true;
	  	   list = [];
	  	   for (var v in set)
	  	      list.push(v);
	  	   return list;
	  	}
	}
}<|endoftext|>


<commit_before>window.addEventListener("load", function() {
    console.log("Hello World!");

    var manifestURL = location.href.substring(0, location.href.lastIndexOf("/")) + "/manifest.webapp";
    // Check if app is installed
    var request = window.navigator.mozApps.checkInstalled(manifestURL);
    request.onerror = function(e) {
        alert("Error calling checkInstalled: " + request.error.name);
    };
    request.onsuccess = function(e) {
        // If app is not installed, install it
        if (!request.result) {
            if (navigator.mozApps) {
                console.log("Manifest url: "+manifestURL);
                var req = navigator.mozApps.install(manifestURL);
                req.onsuccess = function() {
                    console.log(this.result.origin);
                };
                req.onerror = function() {
                    console.log(this.error.name);
                };
            }
        }
    };

});

$(document).ready(function() {
    var sound_effect = new Audio("blip.ogg"); // buffers automatically when created

    console.log("document loaded");

    var score_limit = 70;
    //var score_limit = 20;
    var wordlist = ["foo", "bar", "pizza", "kalsonger", "ananas", "bananer", "kall", "varm"];
    var randomizer = 0;

    var team1_count = 0;
    var team2_count = 0;

    var current_team = 1;
    var countdown_counter = 60;
    //var countdown_counter = 5;

    var current_word = "foo";
    var correct_words = [];
    var dropped_words = [];
    var stolen_words = [];

    prepare_new_game();

    $(document).keypress(function(e){
        if ($("#activeroundpage").is(':visible')) {
            console.log("is visible");
            switch (e.keyCode) {
                case 40:
                    console.log('down');
                    $("#dropword").trigger("click");
                    break;
                case 38:
                    console.log('up');
                    break;
                case 37:
                    console.log('left');
                    if ($("#wordstolen").is(":visible")) {
                        $("#wordstolen").trigger("click");
                    }
                    break;
                case 39:
                    console.log('right');
                    $("#nextword").trigger("click");
                    break;
                default:
                    console.log('???');
                }
        } else {
            console.log("is NOT visible");
        }
    });

    function prepare_new_game() {
        $("#startpage").show();
        $("#winnerresultpage").hide();
        $("#roundpage").hide();
        $("#scorepage").hide();
        $("#scorelimit").val(score_limit);
    }

    $("#startnewgame-button").click(function() {
        score_limit = $("#scorelimit").val();
        console.log("score limit: "+score_limit);
        //var url = "ordlista.txt";
        //var url = "oneword.txt";
        var url = "swe.txt";
        $.ajax({
            url: url,
            success: function (data){
                console.log("done loading");
                wordlist = data.split('\n');
                // Skip last word which is empty because of the last newline
                wordlist.pop();
            }
        });
        start_new_game();
        return false;
    });

    function start_new_game() {
        $("#startpage").hide();
        $("#playpage").show();
        $("#scorepage").show();

        team1_count = 0;
        team2_count = 0;
        current_team = 1;

        prepare_new_round();
    }

    function prepare_new_round() {
        refresh_score();

        $("#startround").show();
        $("#roundpage").show();
        $("#activeroundpage").hide();
        $("#dropword").show();
        $("#wordstolen").hide();
        $("#roundresultpage").hide();

        $("#currentteam").html("Team "+current_team);
    }

    $("#startround").click(function() {
        start_round(current_team);
        return false;
    });

    function start_round(team_count) {
        console.log("Start round!");
        $("#startround").hide();
        $("#activeroundpage").show();

        countdown_counter = 60;
        correct_words = [];
        dropped_words = [];
        stolen_words = [];

        set_new_random_word();

        count_down();
        return false;
    }

    $("#nextword").click(function() {
        correct_words.push(current_word);
        if (countdown_counter > 0) {
            set_new_random_word();
        } else {
            $("#currentword").html("");
            show_results();
        }

        return false;
    });

    $("#dropword").click(function() {
        if (countdown_counter > 0) {
            dropped_words.push(current_word);
            set_new_random_word();
        } else {
            $("#currentword").html("");
            show_results();
        }

        return false;
    });

    $("#wordstolen").click(function() {
        stolen_words.push(current_word);
        $("#currentword").html("");
        show_results();

        return false;
    });

    function set_new_random_word() {
        current_word = get_random_word();
        $("#currentword").html(current_word);
    }

    function count_down() {
        countdown_counter = countdown_counter-1;
        console.log("Count down. Counter: "+countdown_counter);
        $("#timeleft").html(""+countdown_counter);
        if (countdown_counter > 0) {
            setTimeout(count_down, 1000);
        } else {
            finish_round();
        }
    }

    function finish_round() {
        sound_effect.play();
        $("#wordstolen").show();
    }

    function show_results() {
        $("#activeroundpage").hide();
        console.log("Correct words:");
        console.log(correct_words);

        console.log("Skipped words:");
        console.log(dropped_words);

        var correct_words_string = correct_words.join(", ");
        $("#correctwords").html(correct_words_string);
        $("#correctwordscount").val(correct_words.length);

        var dropped_words_string = dropped_words.join(", ");
        $("#droppedwords").html(dropped_words_string);
        $("#droppedwordscount").val(dropped_words.length);

        var stolen_words_string = stolen_words.join(", ");
        $("#stolenwords").html(stolen_words_string);
        $("#stolenwordscount").val(stolen_words.length);

        $("#roundresultpage").show();
    }

    function refresh_score() {
        $("#team1score").html(team1_count);
        $("#team2score").html(team2_count);
    }

    function next_team() {
        if (current_team == 1) {
            current_team = 2;
        } else {
            current_team = 1;
        }
    }

    $("#submitscore").click(function() {
        var correct = parseInt($("#correctwordscount").val());
        var dropped = parseInt($("#droppedwordscount").val());
        var total = correct-dropped;
        var other_team = parseInt($("#stolenwordscount").val());

        if (current_team == 1) {
            team1_count += total;
            team2_count += other_team;
        } else {
            team2_count += total;
            team1_count += other_team;
        }

        console.log("correct: "+correct);
        console.log("dropped: "+dropped);
        console.log("total: "+total);
        console.log("other_team: "+other_team);

        refresh_score();

        if (has_winner()) {
            console.log("Has winner");
            show_winner();
        } else {
            next_team();
            prepare_new_round();
        }

        return false;
    });

    function has_winner() {
        return (team1_count >= score_limit || team2_count >= score_limit);
    }

    function show_winner() {
        console.log("Show winner");
        var winner_team;
        if (team1_count >= score_limit) {
            winner_team = "Team 1";
        } else {
            winner_team = "Team 2";
        }

        $("#roundresultpage").hide();
        $("#winner").html(winner_team);
        $("#winnerresultpage").show();
    }

    $("#playagain-button").click(function() {
        prepare_new_game();
    });

    function get_random_word() {
        var word = wordlist[Math.floor(Math.random()*wordlist.length)];
        console.log("Random word: "+word);
        return word;
    }
});
<commit_msg>Added google analytics<commit_after>window.addEventListener("load", function() {
    console.log("Hello World!");

    var manifestURL = location.href.substring(0, location.href.lastIndexOf("/")) + "/manifest.webapp";
    // Check if app is installed
    var request = window.navigator.mozApps.checkInstalled(manifestURL);
    request.onerror = function(e) {
        alert("Error calling checkInstalled: " + request.error.name);
    };
    request.onsuccess = function(e) {
        // If app is not installed, install it
        if (!request.result) {
            if (navigator.mozApps) {
                console.log("Manifest url: "+manifestURL);
                var req = navigator.mozApps.install(manifestURL);
                req.onsuccess = function() {
                    console.log(this.result.origin);
                };
                req.onerror = function() {
                    console.log(this.error.name);
                };
            }
        }
    };

    // Google analytics
    (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
    (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
    m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
    })(window,document,'script','//www.google-analytics.com/analytics.js','ga');

    ga('create', 'UA-52362355-2', 'auto');
    ga('send', 'pageview');
});

$(document).ready(function() {
    var sound_effect = new Audio("blip.ogg"); // buffers automatically when created

    console.log("document loaded");

    var score_limit = 70;
    //var score_limit = 20;
    var wordlist = ["foo", "bar", "pizza", "kalsonger", "ananas", "bananer", "kall", "varm"];
    var randomizer = 0;

    var team1_count = 0;
    var team2_count = 0;

    var current_team = 1;
    var countdown_counter = 60;
    //var countdown_counter = 5;

    var current_word = "foo";
    var correct_words = [];
    var dropped_words = [];
    var stolen_words = [];

    prepare_new_game();

    $(document).keypress(function(e){
        if ($("#activeroundpage").is(':visible')) {
            console.log("is visible");
            switch (e.keyCode) {
                case 40:
                    console.log('down');
                    $("#dropword").trigger("click");
                    break;
                case 38:
                    console.log('up');
                    break;
                case 37:
                    console.log('left');
                    if ($("#wordstolen").is(":visible")) {
                        $("#wordstolen").trigger("click");
                    }
                    break;
                case 39:
                    console.log('right');
                    $("#nextword").trigger("click");
                    break;
                default:
                    console.log('???');
                }
        } else {
            console.log("is NOT visible");
        }
    });

    function prepare_new_game() {
        $("#startpage").show();
        $("#winnerresultpage").hide();
        $("#roundpage").hide();
        $("#scorepage").hide();
        $("#scorelimit").val(score_limit);
    }

    $("#startnewgame-button").click(function() {
        score_limit = $("#scorelimit").val();
        console.log("score limit: "+score_limit);
        //var url = "ordlista.txt";
        //var url = "oneword.txt";
        var url = "swe.txt";
        $.ajax({
            url: url,
            success: function (data){
                console.log("done loading");
                wordlist = data.split('\n');
                // Skip last word which is empty because of the last newline
                wordlist.pop();
            }
        });
        start_new_game();
        return false;
    });

    function start_new_game() {
        $("#startpage").hide();
        $("#playpage").show();
        $("#scorepage").show();

        team1_count = 0;
        team2_count = 0;
        current_team = 1;

        prepare_new_round();
    }

    function prepare_new_round() {
        refresh_score();

        $("#startround").show();
        $("#roundpage").show();
        $("#activeroundpage").hide();
        $("#dropword").show();
        $("#wordstolen").hide();
        $("#roundresultpage").hide();

        $("#currentteam").html("Team "+current_team);
    }

    $("#startround").click(function() {
        start_round(current_team);
        return false;
    });

    function start_round(team_count) {
        console.log("Start round!");
        $("#startround").hide();
        $("#activeroundpage").show();

        countdown_counter = 60;
        correct_words = [];
        dropped_words = [];
        stolen_words = [];

        set_new_random_word();

        count_down();
        return false;
    }

    $("#nextword").click(function() {
        correct_words.push(current_word);
        if (countdown_counter > 0) {
            set_new_random_word();
        } else {
            $("#currentword").html("");
            show_results();
        }

        return false;
    });

    $("#dropword").click(function() {
        if (countdown_counter > 0) {
            dropped_words.push(current_word);
            set_new_random_word();
        } else {
            $("#currentword").html("");
            show_results();
        }

        return false;
    });

    $("#wordstolen").click(function() {
        stolen_words.push(current_word);
        $("#currentword").html("");
        show_results();

        return false;
    });

    function set_new_random_word() {
        current_word = get_random_word();
        $("#currentword").html(current_word);
    }

    function count_down() {
        countdown_counter = countdown_counter-1;
        console.log("Count down. Counter: "+countdown_counter);
        $("#timeleft").html(""+countdown_counter);
        if (countdown_counter > 0) {
            setTimeout(count_down, 1000);
        } else {
            finish_round();
        }
    }

    function finish_round() {
        sound_effect.play();
        $("#wordstolen").show();
    }

    function show_results() {
        $("#activeroundpage").hide();
        console.log("Correct words:");
        console.log(correct_words);

        console.log("Skipped words:");
        console.log(dropped_words);

        var correct_words_string = correct_words.join(", ");
        $("#correctwords").html(correct_words_string);
        $("#correctwordscount").val(correct_words.length);

        var dropped_words_string = dropped_words.join(", ");
        $("#droppedwords").html(dropped_words_string);
        $("#droppedwordscount").val(dropped_words.length);

        var stolen_words_string = stolen_words.join(", ");
        $("#stolenwords").html(stolen_words_string);
        $("#stolenwordscount").val(stolen_words.length);

        $("#roundresultpage").show();
    }

    function refresh_score() {
        $("#team1score").html(team1_count);
        $("#team2score").html(team2_count);
    }

    function next_team() {
        if (current_team == 1) {
            current_team = 2;
        } else {
            current_team = 1;
        }
    }

    $("#submitscore").click(function() {
        var correct = parseInt($("#correctwordscount").val());
        var dropped = parseInt($("#droppedwordscount").val());
        var total = correct-dropped;
        var other_team = parseInt($("#stolenwordscount").val());

        if (current_team == 1) {
            team1_count += total;
            team2_count += other_team;
        } else {
            team2_count += total;
            team1_count += other_team;
        }

        console.log("correct: "+correct);
        console.log("dropped: "+dropped);
        console.log("total: "+total);
        console.log("other_team: "+other_team);

        refresh_score();

        if (has_winner()) {
            console.log("Has winner");
            show_winner();
        } else {
            next_team();
            prepare_new_round();
        }

        return false;
    });

    function has_winner() {
        return (team1_count >= score_limit || team2_count >= score_limit);
    }

    function show_winner() {
        console.log("Show winner");
        var winner_team;
        if (team1_count >= score_limit) {
            winner_team = "Team 1";
        } else {
            winner_team = "Team 2";
        }

        $("#roundresultpage").hide();
        $("#winner").html(winner_team);
        $("#winnerresultpage").show();
    }

    $("#playagain-button").click(function() {
        prepare_new_game();
    });

    function get_random_word() {
        var word = wordlist[Math.floor(Math.random()*wordlist.length)];
        console.log("Random word: "+word);
        return word;
    }
});
<|endoftext|>


<commit_before>define([
    'jquery',
    'underscore',
    'backbone',
    'moment',
    './tools',
    './traits'
], function($, _, Backbone, moment, Tools, Traits) {
    'use strict';
    var exp = {};

    // =================
    // = MonthCalendar =
    // =================
    var MonthCalendarModel = Traits.Model.extend({
        traits: {
            date: new Traits.Date(),
            fill: new Traits.Bool()
        },
        defaults: function() {
            return {
                date: new Date(),
                fill: false
            };
        }
    });

    /**
     * A month calendar.
     */
    exp.MonthCalendar = Tools.View.extend({
        className: 'tiki-calendar',

        template: _.template(''+
            '
'+
                '
'+
                    '

'+
                '
'+
                '

'+
            '
'
        ),
        templateHeader: _.template(''+
            '
<%= monthname %>, <%= year %>
'+
            '<button class="prevmonth"></button>'+
            '<button class="nextmonth"></button>'
        ),
        templateDay: _.template(''+
            '<td class="<%= cls %>" data-ymd="<%= ymd %>">'+
                '
 '+
                    '
<%= date %>
'+
                '
'+
            '	'
        ),
        events: {
            // 'keydown': 'onKeyDown',
            'click .prevmonth': 'showPrevMonth',
            'click .nextmonth': 'showNextMonth'
        },

        initialize: function(config) {
            config = config || {};
            this.model = config.model || new MonthCalendarModel(config, {parse:true});
            this.listenTo(this.model, 'change', this.onModelChange, this);
        },
        onModelChange: function(model) {
            var attrs = model.changedAttributes();
            this.render();
        },
        showNextMonth: function() {
            var date = this.model.get('date');
            this.model.set('date', moment(date).clone().add({months: 1}).toDate());
        },
        showPrevMonth: function() {
            var date = this.model.get('date');
            this.model.set('date', moment(date).clone().subtract({months: 1}).toDate());
        },

        render: function() {
            var date = moment(this.model.get('date')),
                m = date.clone(),
                month = m.month(),
                today = moment(new Date()),
                today_month = today.month(),
                today_date = today.date();

            // Start at first day of month
            m.date(1);
            var firstWeekdayOfMonth = m.day() - 1;
            if(firstWeekdayOfMonth == -1)
                firstWeekdayOfMonth = 6;
            m.subtract('days', firstWeekdayOfMonth);

            // Create the main table
            var table = $(this.template({monthname: date.format('MMMM')}));
            var tbody = table.children('tbody');

            // Render the header
            table.find('thead .header').html(this.templateHeader({
                monthname: date.format('MMMM'),
                year: date.format('YYYY')
            }));

            // Add the weekday names
            var tr = $('

').appendTo(table.find('thead'));
            for(var i=0,days=[1,2,3,4,5,6,0]; i<days.length; i++) {
                tr.append($(''+moment.weekdaysShort()[days[i]]+'	'));
            }

            // Add all the days
            for(i=0; i<42; i++) {
                if(i % 7 === 0) {
                   tr = $('

').appendTo(tbody);
                }

                // Add optional cell styling
                var cls = ['day'],
                    day = m.day();
                if(m.month() !== month) {
                    cls.push('gray');
                }
                if(day == 6 || day === 0) { // saturday or sunday
                    cls.push('weekend');
                }
                if(m.date() == today_date && m.month() == today_month) {
                    cls.push('today');
                }

                // Draw the cell
                if(!this.fill && m.month() !== month) {
                    // Only show days in this month
                    tr.append('

	');
                } else {
                    // tr.append('
'+(m.date())+'
	');
                    var html = $(this.templateDay({
                        cls: cls.join(' '),
                        date: m.date(),
                        ymd: m.format('YYYY-MM-DD')
                    }));
                    tr.append(html);
                }
                m.add('days', 1);
            }

            $(this.el).empty().append(table);

            return this;
        }

    });

    return exp;
});<commit_msg>Use today's date if rendering a month calendar with no specified date<commit_after>define([
    'jquery',
    'underscore',
    'backbone',
    'moment',
    './tools',
    './traits'
], function($, _, Backbone, moment, Tools, Traits) {
    'use strict';
    var exp = {};

    // =================
    // = MonthCalendar =
    // =================
    var MonthCalendarModel = Traits.Model.extend({
        traits: {
            date: new Traits.Date(),
            fill: new Traits.Bool()
        },
        defaults: function() {
            return {
                date: new Date(),
                fill: false
            };
        }
    });

    /**
     * A month calendar.
     */
    exp.MonthCalendar = Tools.View.extend({
        className: 'tiki-calendar',

        template: _.template(''+
            '
'+
                '
'+
                    '

'+
                '
'+
                '

'+
            '
'
        ),
        templateHeader: _.template(''+
            '
<%= monthname %>, <%= year %>
'+
            '<button class="prevmonth"></button>'+
            '<button class="nextmonth"></button>'
        ),
        templateDay: _.template(''+
            '<td class="<%= cls %>" data-ymd="<%= ymd %>">'+
                '
 '+
                    '
<%= date %>
'+
                '
'+
            '	'
        ),
        events: {
            // 'keydown': 'onKeyDown',
            'click .prevmonth': 'showPrevMonth',
            'click .nextmonth': 'showNextMonth'
        },

        initialize: function(config) {
            config = config || {};
            this.model = config.model || new MonthCalendarModel(config, {parse:true});
            this.listenTo(this.model, 'change', this.onModelChange, this);
        },
        onModelChange: function(model) {
            var attrs = model.changedAttributes();
            this.render();
        },
        showNextMonth: function() {
            var date = this.model.get('date');
            this.model.set('date', moment(date).clone().add({months: 1}).toDate());
        },
        showPrevMonth: function() {
            var date = this.model.get('date');
            this.model.set('date', moment(date).clone().subtract({months: 1}).toDate());
        },

        render: function() {
            var date = moment(this.model.get('date') || new Date()),
                m = date.clone(),
                month = m.month(),
                today = moment(new Date()),
                today_month = today.month(),
                today_date = today.date();

            // Start at first day of month
            m.date(1);
            var firstWeekdayOfMonth = m.day() - 1;
            if(firstWeekdayOfMonth == -1)
                firstWeekdayOfMonth = 6;
            m.subtract('days', firstWeekdayOfMonth);

            // Create the main table
            var table = $(this.template({monthname: date.format('MMMM')}));
            var tbody = table.children('tbody');

            // Render the header
            table.find('thead .header').html(this.templateHeader({
                monthname: date.format('MMMM'),
                year: date.format('YYYY')
            }));

            // Add the weekday names
            var tr = $('

').appendTo(table.find('thead'));
            for(var i=0,days=[1,2,3,4,5,6,0]; i<days.length; i++) {
                tr.append($(''+moment.weekdaysShort()[days[i]]+'	'));
            }

            // Add all the days
            for(i=0; i<42; i++) {
                if(i % 7 === 0) {
                   tr = $('

').appendTo(tbody);
                }

                // Add optional cell styling
                var cls = ['day'],
                    day = m.day();
                if(m.month() !== month) {
                    cls.push('gray');
                }
                if(day == 6 || day === 0) { // saturday or sunday
                    cls.push('weekend');
                }
                if(m.date() == today_date && m.month() == today_month) {
                    cls.push('today');
                }

                // Draw the cell
                if(!this.fill && m.month() !== month) {
                    // Only show days in this month
                    tr.append('

	');
                } else {
                    // tr.append('
'+(m.date())+'
	');
                    var html = $(this.templateDay({
                        cls: cls.join(' '),
                        date: m.date(),
                        ymd: m.format('YYYY-MM-DD')
                    }));
                    tr.append(html);
                }
                m.add('days', 1);
            }

            $(this.el).empty().append(table);

            return this;
        }

    });

    return exp;
});<|endoftext|>


<commit_before>/**
 * Mocks indexing Db adapter
 */
var _ = require('lodash');
var Db = require('backbone-db-local');
var debug = require('../utils/debug')('serverbone:utils:db:mock');
var async = require('async');

var IndexingTestDb = function TestDb() {
  var db = Db.apply(this, arguments);
  this.storage = db.storage;
  this.records = db.records;
};

_.extend(IndexingTestDb.prototype, Db.prototype, {
  addToIndex: function(collection, model, options, cb) {
    var self = this;
    debug.log('add ' + model.id + ' to ' + options.indexKey);
    this.store().getItem(options.indexKey, function(err, items) {
      if (err) return cb(err);
      if (!items) {
        items = [];
      } else {
        items = JSON.parse(items);
      }
      // item was already existing
      if (items.indexOf(model.id) > -1) {
        return cb();
      }
      items.push(model.id);
      self.store().setItem(
        options.indexKey,
        JSON.stringify(items),
        function(err, res) {
          cb(err, model.toJSON(), res);
        });
    });
  },

  readFromIndex: function(collection, options, cb) {
    this.store().getItem(options.indexKey, function(err, ids) {
      ids = _.isString(ids) ? JSON.parse(ids) : ids;
      if (options.limit) {
        ids = ids.splice(options.offset || 0, options.limit);
      }
      var models = [];
      _.each(ids, function(id) {
        models.push({
          id: id
        });
      });
      collection.set(models, options);
      return cb(err, models);
    });
  },

  readFromIndexes: function(collection, options, cb) {
    var indexesToRead = options.indexKeys;
    var fns = _.map(indexesToRead, function(key) {
      var store = this.store();
      return function getItem(cb) {
        store.getItem(key, function(err, ids) {
          cb(err, ids);
        });
      };
    }, this);

    async.parallel(fns, function(err, results) {
      if (err) return cb(err);
      // join results
      var models = [];
      _.each(results, function(ids) {
        ids = _.isString(ids) ? JSON.parse(ids) : ids;
        _.each(ids, function(id) {
          models.push({
            id: id
          });
        });
      });
      collection.set(models, options);
      return cb(err, models);
    });
  },

  removeFromIndex: function(collection, models, options, cb) {
    var self = this;
    var ids = _.pluck(models, models[0].idAttribute);
    debug.log('removing ' + ids + ' from ' + options.indexKey);
    this.store().getItem(options.indexKey, function(err, items) {
      items = _.isString(items) ? JSON.parse(items) : items;
      items = _.reject(items, function(id) {
        return ids.indexOf(id) > -1;
      });
      self.store().setItem(
        options.indexKey,
        JSON.stringify(items),
        function(err, res) {
          cb(err, models, res);
        }
      );
    });
  },

  // removes everything from index
  removeIndex: function(collection, options, cb) {
    this.store().setItem(
      options.indexKey,
      JSON.stringify([]),
      function(err, res) {
        cb(err, [], res);
      }
    );
  },

  existsInIndex: function(collection, model, options, cb) {
    this.store().getItem(options.indexKey, function(err, ids) {
      ids = ids ? JSON.parse(ids) : [];
      cb(err, ids.indexOf(model.id) > -1);
    });
  },

  indexCount: function(collection, options, cb) {
    this.store().getItem(options.indexKey, function(err, ids) {
      ids = ids ? JSON.parse(ids) : [];
      cb(err, ids.length);
    });
  },

  // mock adapter only supports exact match
  findKeys: function(collection, options, cb) {
    var ids = [];
    var keys = _.isFunction(collection.url) ? collection.url() : collection.url;
    keys += options.keys;
    debug.log('findKeys', keys);
    this.store().getItem(keys, function(err, data) {
      if (data) ids.push(keys);
      cb(null, ids);
    });
  }
});

IndexingTestDb.sync = IndexingTestDb.prototype.sync;

module.exports = IndexingTestDb;<commit_msg>sortOrder support for mock readFromIndexes<commit_after>/**
 * Mocks indexing Db adapter
 */
var _ = require('lodash');
var Db = require('backbone-db-local');
var debug = require('../utils/debug')('serverbone:utils:db:mock');
var async = require('async');

var IndexingTestDb = function TestDb() {
  var db = Db.apply(this, arguments);
  this.storage = db.storage;
  this.records = db.records;
};

_.extend(IndexingTestDb.prototype, Db.prototype, {
  addToIndex: function(collection, model, options, cb) {
    var self = this;
    debug.log('add ' + model.id + ' to ' + options.indexKey);
    this.store().getItem(options.indexKey, function(err, items) {
      if (err) return cb(err);
      if (!items) {
        items = [];
      } else {
        items = JSON.parse(items);
      }
      // item was already existing
      if (items.indexOf(model.id) > -1) {
        return cb();
      }
      items.push(model.id);
      self.store().setItem(
        options.indexKey,
        JSON.stringify(items),
        function(err, res) {
          cb(err, model.toJSON(), res);
        });
    });
  },

  readFromIndex: function(collection, options, cb) {
    this.store().getItem(options.indexKey, function(err, ids) {
      ids = _.isString(ids) ? JSON.parse(ids) : ids;
      if (options.sortOrder === 1) ids.reverse();
      if (options.limit) {
        ids = ids.splice(options.offset || 0, options.limit);
      }
      var models = [];
      _.each(ids, function(id) {
        models.push({
          id: id
        });
      });
      collection.set(models, options);
      return cb(err, models);
    });
  },

  readFromIndexes: function(collection, options, cb) {
    var indexesToRead = options.indexKeys;
    var fns = _.map(indexesToRead, function(key) {
      var store = this.store();
      return function getItem(cb) {
        store.getItem(key, function(err, ids) {
          cb(err, ids);
        });
      };
    }, this);

    async.parallel(fns, function(err, results) {
      if (err) return cb(err);
      // join results
      var models = [];
      _.each(results, function(ids) {
        ids = _.isString(ids) ? JSON.parse(ids) : ids;
        _.each(ids, function(id) {
          models.push({
            id: id
          });
        });
      });
      collection.set(models, options);
      return cb(err, models);
    });
  },

  removeFromIndex: function(collection, models, options, cb) {
    var self = this;
    var ids = _.pluck(models, models[0].idAttribute);
    debug.log('removing ' + ids + ' from ' + options.indexKey);
    this.store().getItem(options.indexKey, function(err, items) {
      items = _.isString(items) ? JSON.parse(items) : items;
      items = _.reject(items, function(id) {
        return ids.indexOf(id) > -1;
      });
      self.store().setItem(
        options.indexKey,
        JSON.stringify(items),
        function(err, res) {
          cb(err, models, res);
        }
      );
    });
  },

  // removes everything from index
  removeIndex: function(collection, options, cb) {
    this.store().setItem(
      options.indexKey,
      JSON.stringify([]),
      function(err, res) {
        cb(err, [], res);
      }
    );
  },

  existsInIndex: function(collection, model, options, cb) {
    this.store().getItem(options.indexKey, function(err, ids) {
      ids = ids ? JSON.parse(ids) : [];
      cb(err, ids.indexOf(model.id) > -1);
    });
  },

  indexCount: function(collection, options, cb) {
    this.store().getItem(options.indexKey, function(err, ids) {
      ids = ids ? JSON.parse(ids) : [];
      cb(err, ids.length);
    });
  },

  // mock adapter only supports exact match
  findKeys: function(collection, options, cb) {
    var ids = [];
    var keys = _.isFunction(collection.url) ? collection.url() : collection.url;
    keys += options.keys;
    debug.log('findKeys', keys);
    this.store().getItem(keys, function(err, data) {
      if (data) ids.push(keys);
      cb(null, ids);
    });
  }
});

IndexingTestDb.sync = IndexingTestDb.prototype.sync;

module.exports = IndexingTestDb;<|endoftext|>


<commit_before>Ext.define('FromLabs.extends.direct.DartProvider', {
  extend: Ext.direct.RemotingProvider,
  alias:  'direct.dartprovider',

  sendRequest: function(data) {
    var me = this,
        request, callData, params,
        i, len;

    request = {
        url: me.url,
        callback: me.onData,
        scope: me,
        transaction: data
    };

    if (Ext.isArray(data)) {
        callData = [];

        for (i = 0, len = data.length; i < len; ++i) {
            callData.push(me.getCallData(data[i]));
        }
    }
    else {
        callData = me.getCallData(data);
    }

    request.jsonData = callData;

    me.dartRequest(request);
  },

  sendFormRequest: function(transaction) {
    var me = this;

    dartRequest({
        url: me.url,
        params: transaction.params,
        callback: me.onData,
        scope: me,
        form: transaction.form,
        isUpload: transaction.isUpload,
        transaction: transaction
    });
  },

  dartRequest: function(request) {
    directCall(null, "direct", Ext.JSON.encode(request.jsonData), function(result) {
      var response = {
        responseText: Ext.JSON.decode(result)
      };
      Ext.callback(request.callback, request.scope, [request, true, response]);
    });
  }
});
<commit_msg>Fix extension<commit_after>Ext.define('FromLabs.extends.direct.DartProvider', {
  extend: "Ext.direct.RemotingProvider",
  alias:  'direct.dartprovider',

  sendRequest: function(data) {
    var me = this,
        request, callData, params,
        i, len;

    request = {
        url: me.url,
        callback: me.onData,
        scope: me,
        transaction: data
    };

    if (Ext.isArray(data)) {
        callData = [];

        for (i = 0, len = data.length; i < len; ++i) {
            callData.push(me.getCallData(data[i]));
        }
    }
    else {
        callData = me.getCallData(data);
    }

    request.jsonData = callData;

    me.dartRequest(request);
  },

  sendFormRequest: function(transaction) {
    var me = this;

    dartRequest({
        url: me.url,
        params: transaction.params,
        callback: me.onData,
        scope: me,
        form: transaction.form,
        isUpload: transaction.isUpload,
        transaction: transaction
    });
  },

  dartRequest: function(request) {
    directCall(null, "direct", Ext.JSON.encode(request.jsonData), function(result) {
      var response = {
        responseText: Ext.JSON.decode(result)
      };
      Ext.callback(request.callback, request.scope, [request, true, response]);
    });
  }
});
<|endoftext|>


<commit_before>import CsvGen from './csvGen'
const { app } = require('electron').remote
const fs = require('fs')

const configBasePath = `${app.getPath('userData')}/upstream`
fs.existsSync(configBasePath) || fs.mkdirSync(configBasePath)

export default {
  saveTmp: function (recordId, reportData) {
    const tmpPath = `${configBasePath}/${recordId}.json`
    fs.writeFileSync(tmpPath, JSON.stringify(reportData))
  },
  saveAs: function (fileName, reportData) {
    fs.writeFileSync(fileName, JSON.stringify(reportData))
  },
  saveAsCsv: function (filename, siteData, flowData, results) {
    const csv = new CsvGen(siteData, flowData, results)
    const report = csv.genReport()
    fs.writeFileSync(filename, report)
  }
}
<commit_msg>Fix tmp file path<commit_after>import CsvGen from './csvGen'
const { app } = require('electron').remote
const fs = require('fs')

const configBasePath = `${app.getPath('userData')}`
fs.existsSync(configBasePath) || fs.mkdirSync(configBasePath)

export default {
  saveTmp: function (recordId, reportData) {
    const tmpPath = `${configBasePath}/${recordId}.json`
    fs.writeFileSync(tmpPath, JSON.stringify(reportData))
  },
  saveAs: function (fileName, reportData) {
    fs.writeFileSync(fileName, JSON.stringify(reportData))
  },
  saveAsCsv: function (filename, siteData, flowData, results) {
    const csv = new CsvGen(siteData, flowData, results)
    const report = csv.genReport()
    fs.writeFileSync(filename, report)
  }
}
<|endoftext|>


<commit_before>// To keep the UI snappy, we run WebTorrent in its own hidden window, a separate
// process from the main window.
console.time('init')

const crypto = require('crypto')
const deepEqual = require('deep-equal')
const defaultAnnounceList = require('create-torrent').announceList
const electron = require('electron')
const fs = require('fs-extra')
const musicmetadata = require('musicmetadata')
const networkAddress = require('network-address')
const path = require('path')
const WebTorrent = require('webtorrent')
const zeroFill = require('zero-fill')

const crashReporter = require('../crash-reporter')
const config = require('../config')
const {TorrentKeyNotFoundError} = require('./lib/errors')
const torrentPoster = require('./lib/torrent-poster')

// Report when the process crashes
crashReporter.init()

// Send & receive messages from the main window
const ipc = electron.ipcRenderer

// Force use of webtorrent trackers on all torrents
global.WEBTORRENT_ANNOUNCE = defaultAnnounceList
  .map((arr) => arr[0])
  .filter((url) => url.indexOf('wss://') === 0 || url.indexOf('ws://') === 0)

/**
 * WebTorrent version.
 */
const VERSION = require('../../package.json').version

/**
 * Version number in Azureus-style. Generated from major and minor semver version.
 * For example:
 *   '0.16.1' -> '0016'
 *   '1.2.5' -> '0102'
 */
const VERSION_STR = VERSION.match(/([0-9]+)/g)
  .slice(0, 2)
  .map((v) => zeroFill(2, v))
  .join('')

/**
 * Version prefix string (used in peer ID). WebTorrent uses the Azureus-style
 * encoding: '-', two characters for client id ('WW'), four ascii digits for version
 * number, '-', followed by random numbers.
 * For example:
 *   '-WW0102-'...
 */
const VERSION_PREFIX = '-WD' + VERSION_STR + '-'

/**
 * Generate an ephemeral peer ID each time.
 * Once there are around 2^24 = ~8 million WebTorrent Desktops online at the same time,
 * ID collisions will start happening. Birthday paradox.
 * This is fine, though. Bad peers can already clone someone else's peer ID.
 * The network is robust to occasional collisions.
 */
const PEER_ID = Buffer.from(VERSION_PREFIX + crypto.randomBytes(6).toString('hex'))

// Connect to the WebTorrent and BitTorrent networks. WebTorrent Desktop is a hybrid
// client, as explained here: https://webtorrent.io/faq
let client = window.client = new WebTorrent({ peerId: PEER_ID })

// WebTorrent-to-HTTP streaming sever
let server = null

// Used for diffing, so we only send progress updates when necessary
let prevProgress = null

init()

function init () {
  listenToClientEvents()

  ipc.on('wt-start-torrenting', (e, torrentKey, torrentID, path, fileModtimes, selections) =>
    startTorrenting(torrentKey, torrentID, path, fileModtimes, selections))
  ipc.on('wt-stop-torrenting', (e, infoHash) =>
    stopTorrenting(infoHash))
  ipc.on('wt-create-torrent', (e, torrentKey, options) =>
    createTorrent(torrentKey, options))
  ipc.on('wt-save-torrent-file', (e, torrentKey) =>
    saveTorrentFile(torrentKey))
  ipc.on('wt-generate-torrent-poster', (e, torrentKey) =>
    generateTorrentPoster(torrentKey))
  ipc.on('wt-get-audio-metadata', (e, infoHash, index) =>
    getAudioMetadata(infoHash, index))
  ipc.on('wt-start-server', (e, infoHash) =>
    startServer(infoHash))
  ipc.on('wt-stop-server', (e) =>
    stopServer())
  ipc.on('wt-select-files', (e, infoHash, selections) =>
    selectFiles(infoHash, selections))

  ipc.send('ipcReadyWebTorrent')

  window.addEventListener('error', (e) =>
    ipc.send('wt-uncaught-error', {message: e.error.message, stack: e.error.stack}),
    true)

  setInterval(updateTorrentProgress, 1000)
  console.timeEnd('init')
}

function listenToClientEvents () {
  client.on('warning', (err) => ipc.send('wt-warning', null, err.message))
  client.on('error', (err) => ipc.send('wt-error', null, err.message))
}

// Starts a given TorrentID, which can be an infohash, magnet URI, etc.
// Returns a WebTorrent object. See https://git.io/vik9M
function startTorrenting (torrentKey, torrentID, path, fileModtimes, selections) {
  console.log('starting torrent %s: %s', torrentKey, torrentID)

  const torrent = client.add(torrentID, {
    path: path,
    fileModtimes: fileModtimes
  })
  torrent.key = torrentKey

  // Listen for ready event, progress notifications, etc
  addTorrentEvents(torrent)

  // Only download the files the user wants, not necessarily all files
  torrent.once('ready', () => selectFiles(torrent, selections))

  return torrent
}

function stopTorrenting (infoHash) {
  const torrent = client.get(infoHash)
  if (torrent) torrent.destroy()
}

// Create a new torrent, start seeding
function createTorrent (torrentKey, options) {
  console.log('creating torrent', torrentKey, options)
  const paths = options.files.map((f) => f.path)
  const torrent = client.seed(paths, options)
  torrent.key = torrentKey
  addTorrentEvents(torrent)
  ipc.send('wt-new-torrent')
}

function addTorrentEvents (torrent) {
  torrent.on('warning', (err) =>
    ipc.send('wt-warning', torrent.key, err.message))
  torrent.on('error', (err) =>
    ipc.send('wt-error', torrent.key, err.message))
  torrent.on('infoHash', () =>
    ipc.send('wt-infohash', torrent.key, torrent.infoHash))
  torrent.on('metadata', torrentMetadata)
  torrent.on('ready', torrentReady)
  torrent.on('done', torrentDone)

  function torrentMetadata () {
    const info = getTorrentInfo(torrent)
    ipc.send('wt-metadata', torrent.key, info)

    updateTorrentProgress()
  }

  function torrentReady () {
    const info = getTorrentInfo(torrent)
    ipc.send('wt-ready', torrent.key, info)
    ipc.send('wt-ready-' + torrent.infoHash, torrent.key, info)

    updateTorrentProgress()
  }

  function torrentDone () {
    const info = getTorrentInfo(torrent)
    ipc.send('wt-done', torrent.key, info)

    updateTorrentProgress()

    torrent.getFileModtimes(function (err, fileModtimes) {
      if (err) return onError(err)
      ipc.send('wt-file-modtimes', torrent.key, fileModtimes)
    })
  }
}

// Produces a JSON saveable summary of a torrent
function getTorrentInfo (torrent) {
  return {
    infoHash: torrent.infoHash,
    magnetURI: torrent.magnetURI,
    name: torrent.name,
    path: torrent.path,
    files: torrent.files.map(getTorrentFileInfo),
    bytesReceived: torrent.received
  }
}

// Produces a JSON saveable summary of a file in a torrent
function getTorrentFileInfo (file) {
  return {
    name: file.name,
    length: file.length,
    path: file.path
  }
}

// Every time we resolve a magnet URI, save the torrent file so that we never
// have to download it again. Never ask the DHT the same question twice.
function saveTorrentFile (torrentKey) {
  const torrent = getTorrent(torrentKey)
  checkIfTorrentFileExists(torrent.infoHash, function (torrentPath, exists) {
    const fileName = torrent.infoHash + '.torrent'
    if (exists) {
      // We've already saved the file
      return ipc.send('wt-file-saved', torrentKey, fileName)
    }

    // Otherwise, save the .torrent file, under the app config folder
    fs.mkdir(config.TORRENT_PATH, function (_) {
      fs.writeFile(torrentPath, torrent.torrentFile, function (err) {
        if (err) return console.log('error saving torrent file %s: %o', torrentPath, err)
        console.log('saved torrent file %s', torrentPath)
        return ipc.send('wt-file-saved', torrentKey, fileName)
      })
    })
  })
}

// Checks whether we've already resolved a given infohash to a torrent file
// Calls back with (torrentPath, exists). Logs, does not call back on error
function checkIfTorrentFileExists (infoHash, cb) {
  const torrentPath = path.join(config.TORRENT_PATH, infoHash + '.torrent')
  fs.exists(torrentPath, function (exists) {
    cb(torrentPath, exists)
  })
}

// Save a JPG that represents a torrent.
// Auto chooses either a frame from a video file, an image, etc
function generateTorrentPoster (torrentKey) {
  const torrent = getTorrent(torrentKey)
  torrentPoster(torrent, function (err, buf, extension) {
    if (err) return console.log('error generating poster: %o', err)
    // save it for next time
    fs.mkdirp(config.POSTER_PATH, function (err) {
      if (err) return console.log('error creating poster dir: %o', err)
      const posterFileName = torrent.infoHash + extension
      const posterFilePath = path.join(config.POSTER_PATH, posterFileName)
      fs.writeFile(posterFilePath, buf, function (err) {
        if (err) return console.log('error saving poster: %o', err)
        // show the poster
        ipc.send('wt-poster', torrentKey, posterFileName)
      })
    })
  })
}

function updateTorrentProgress () {
  const progress = getTorrentProgress()
  // TODO: diff torrent-by-torrent, not once for the whole update
  if (prevProgress && deepEqual(progress, prevProgress, {strict: true})) {
    return /* don't send heavy object if it hasn't changed */
  }
  ipc.send('wt-progress', progress)
  prevProgress = progress
}

function getTorrentProgress () {
  // First, track overall progress
  const progress = client.progress
  const hasActiveTorrents = client.torrents.some(function (torrent) {
    return torrent.progress !== 1
  })

  // Track progress for every file in each torrent
  // TODO: ideally this would be tracked by WebTorrent, which could do it
  // more efficiently than looping over torrent.bitfield
  const torrentProg = client.torrents.map(function (torrent) {
    const fileProg = torrent.files && torrent.files.map(function (file, index) {
      const numPieces = file._endPiece - file._startPiece + 1
      let numPiecesPresent = 0
      for (let piece = file._startPiece; piece <= file._endPiece; piece++) {
        if (torrent.bitfield.get(piece)) numPiecesPresent++
      }
      return {
        startPiece: file._startPiece,
        endPiece: file._endPiece,
        numPieces,
        numPiecesPresent
      }
    })
    return {
      torrentKey: torrent.key,
      ready: torrent.ready,
      progress: torrent.progress,
      downloaded: torrent.downloaded,
      downloadSpeed: torrent.downloadSpeed,
      uploadSpeed: torrent.uploadSpeed,
      numPeers: torrent.numPeers,
      length: torrent.length,
      bitfield: torrent.bitfield,
      files: fileProg
    }
  })

  return {
    torrents: torrentProg,
    progress,
    hasActiveTorrents
  }
}

function startServer (infoHash) {
  const torrent = client.get(infoHash)
  if (torrent.ready) startServerFromReadyTorrent(torrent)
  else torrent.once('ready', () => startServerFromReadyTorrent(torrent))
}

function startServerFromReadyTorrent (torrent, cb) {
  if (server) return

  // start the streaming torrent-to-http server
  server = torrent.createServer()
  server.listen(0, function () {
    const port = server.address().port
    const urlSuffix = ':' + port
    const info = {
      torrentKey: torrent.key,
      localURL: 'http://localhost' + urlSuffix,
      networkURL: 'http://' + networkAddress() + urlSuffix
    }

    ipc.send('wt-server-running', info)
    ipc.send('wt-server-' + torrent.infoHash, info)
  })
}

function stopServer () {
  if (!server) return
  server.destroy()
  server = null
}

function getAudioMetadata (infoHash, index) {
  const torrent = client.get(infoHash)
  const file = torrent.files[index]
  musicmetadata(file.createReadStream(), function (err, info) {
    if (err) return console.log('error getting audio metadata for ' + infoHash + ':' + index, err)
    const { artist, album, albumartist, title, year, track, disk, genre } = info
    const importantInfo = { artist, album, albumartist, title, year, track, disk, genre }
    console.log('got audio metadata for %s: %o', file.name, importantInfo)
    ipc.send('wt-audio-metadata', infoHash, index, importantInfo)
  })
}

function selectFiles (torrentOrInfoHash, selections) {
  // Get the torrent object
  let torrent
  if (typeof torrentOrInfoHash === 'string') {
    torrent = client.get(torrentOrInfoHash)
  } else {
    torrent = torrentOrInfoHash
  }
  if (!torrent) {
    throw new Error('selectFiles: missing torrent ' + torrentOrInfoHash)
  }

  // Selections not specified?
  // Load all files. We still need to replace the default whole-torrent
  // selection with individual selections for each file, so we can
  // select/deselect files later on
  if (!selections) {
    selections = torrent.files.map((x) => true)
  }

  // Selections specified incorrectly?
  if (selections.length !== torrent.files.length) {
    throw new Error('got ' + selections.length + ' file selections, ' +
      'but the torrent contains ' + torrent.files.length + ' files')
  }

  // Remove default selection (whole torrent)
  torrent.deselect(0, torrent.pieces.length - 1, false)

  // Add selections (individual files)
  for (let i = 0; i < selections.length; i++) {
    const file = torrent.files[i]
    if (selections[i]) {
      file.select()
    } else {
      console.log('deselecting file ' + i + ' of torrent ' + torrent.name)
      file.deselect()
    }
  }
}

// Gets a WebTorrent handle by torrentKey
// Throws an Error if we're not currently torrenting anything w/ that key
function getTorrent (torrentKey) {
  const ret = client.torrents.find((x) => x.key === torrentKey)
  if (!ret) throw new TorrentKeyNotFoundError(torrentKey)
  return ret
}

function onError (err) {
  console.log(err)
}

// TODO: remove this once the following bugs are fixed:
// https://bugs.chromium.org/p/chromium/issues/detail?id=490143
// https://github.com/electron/electron/issues/7212
window.testOfflineMode = function () {
  console.log('Test, going OFFLINE')
  client = window.client = new WebTorrent({
    peerId: PEER_ID,
    tracker: false,
    dht: false,
    webSeeds: false
  })
  listenToClientEvents()
}
<commit_msg>Add more peer ID entropy (#960)<commit_after>// To keep the UI snappy, we run WebTorrent in its own hidden window, a separate
// process from the main window.
console.time('init')

const crypto = require('crypto')
const deepEqual = require('deep-equal')
const defaultAnnounceList = require('create-torrent').announceList
const electron = require('electron')
const fs = require('fs-extra')
const musicmetadata = require('musicmetadata')
const networkAddress = require('network-address')
const path = require('path')
const WebTorrent = require('webtorrent')
const zeroFill = require('zero-fill')

const crashReporter = require('../crash-reporter')
const config = require('../config')
const {TorrentKeyNotFoundError} = require('./lib/errors')
const torrentPoster = require('./lib/torrent-poster')

// Report when the process crashes
crashReporter.init()

// Send & receive messages from the main window
const ipc = electron.ipcRenderer

// Force use of webtorrent trackers on all torrents
global.WEBTORRENT_ANNOUNCE = defaultAnnounceList
  .map((arr) => arr[0])
  .filter((url) => url.indexOf('wss://') === 0 || url.indexOf('ws://') === 0)

/**
 * WebTorrent version.
 */
const VERSION = require('../../package.json').version

/**
 * Version number in Azureus-style. Generated from major and minor semver version.
 * For example:
 *   '0.16.1' -> '0016'
 *   '1.2.5' -> '0102'
 */
const VERSION_STR = VERSION.match(/([0-9]+)/g)
  .slice(0, 2)
  .map((v) => zeroFill(2, v))
  .join('')

/**
 * Version prefix string (used in peer ID). WebTorrent uses the Azureus-style
 * encoding: '-', two characters for client id ('WW'), four ascii digits for version
 * number, '-', followed by random numbers.
 * For example:
 *   '-WW0102-'...
 */
const VERSION_PREFIX = '-WD' + VERSION_STR + '-'

/**
 * Generate an ephemeral peer ID each time.
 */
const PEER_ID = Buffer.from(VERSION_PREFIX + crypto.randomBytes(9).toString('base64'))

// Connect to the WebTorrent and BitTorrent networks. WebTorrent Desktop is a hybrid
// client, as explained here: https://webtorrent.io/faq
let client = window.client = new WebTorrent({ peerId: PEER_ID })

// WebTorrent-to-HTTP streaming sever
let server = null

// Used for diffing, so we only send progress updates when necessary
let prevProgress = null

init()

function init () {
  listenToClientEvents()

  ipc.on('wt-start-torrenting', (e, torrentKey, torrentID, path, fileModtimes, selections) =>
    startTorrenting(torrentKey, torrentID, path, fileModtimes, selections))
  ipc.on('wt-stop-torrenting', (e, infoHash) =>
    stopTorrenting(infoHash))
  ipc.on('wt-create-torrent', (e, torrentKey, options) =>
    createTorrent(torrentKey, options))
  ipc.on('wt-save-torrent-file', (e, torrentKey) =>
    saveTorrentFile(torrentKey))
  ipc.on('wt-generate-torrent-poster', (e, torrentKey) =>
    generateTorrentPoster(torrentKey))
  ipc.on('wt-get-audio-metadata', (e, infoHash, index) =>
    getAudioMetadata(infoHash, index))
  ipc.on('wt-start-server', (e, infoHash) =>
    startServer(infoHash))
  ipc.on('wt-stop-server', (e) =>
    stopServer())
  ipc.on('wt-select-files', (e, infoHash, selections) =>
    selectFiles(infoHash, selections))

  ipc.send('ipcReadyWebTorrent')

  window.addEventListener('error', (e) =>
    ipc.send('wt-uncaught-error', {message: e.error.message, stack: e.error.stack}),
    true)

  setInterval(updateTorrentProgress, 1000)
  console.timeEnd('init')
}

function listenToClientEvents () {
  client.on('warning', (err) => ipc.send('wt-warning', null, err.message))
  client.on('error', (err) => ipc.send('wt-error', null, err.message))
}

// Starts a given TorrentID, which can be an infohash, magnet URI, etc.
// Returns a WebTorrent object. See https://git.io/vik9M
function startTorrenting (torrentKey, torrentID, path, fileModtimes, selections) {
  console.log('starting torrent %s: %s', torrentKey, torrentID)

  const torrent = client.add(torrentID, {
    path: path,
    fileModtimes: fileModtimes
  })
  torrent.key = torrentKey

  // Listen for ready event, progress notifications, etc
  addTorrentEvents(torrent)

  // Only download the files the user wants, not necessarily all files
  torrent.once('ready', () => selectFiles(torrent, selections))

  return torrent
}

function stopTorrenting (infoHash) {
  const torrent = client.get(infoHash)
  if (torrent) torrent.destroy()
}

// Create a new torrent, start seeding
function createTorrent (torrentKey, options) {
  console.log('creating torrent', torrentKey, options)
  const paths = options.files.map((f) => f.path)
  const torrent = client.seed(paths, options)
  torrent.key = torrentKey
  addTorrentEvents(torrent)
  ipc.send('wt-new-torrent')
}

function addTorrentEvents (torrent) {
  torrent.on('warning', (err) =>
    ipc.send('wt-warning', torrent.key, err.message))
  torrent.on('error', (err) =>
    ipc.send('wt-error', torrent.key, err.message))
  torrent.on('infoHash', () =>
    ipc.send('wt-infohash', torrent.key, torrent.infoHash))
  torrent.on('metadata', torrentMetadata)
  torrent.on('ready', torrentReady)
  torrent.on('done', torrentDone)

  function torrentMetadata () {
    const info = getTorrentInfo(torrent)
    ipc.send('wt-metadata', torrent.key, info)

    updateTorrentProgress()
  }

  function torrentReady () {
    const info = getTorrentInfo(torrent)
    ipc.send('wt-ready', torrent.key, info)
    ipc.send('wt-ready-' + torrent.infoHash, torrent.key, info)

    updateTorrentProgress()
  }

  function torrentDone () {
    const info = getTorrentInfo(torrent)
    ipc.send('wt-done', torrent.key, info)

    updateTorrentProgress()

    torrent.getFileModtimes(function (err, fileModtimes) {
      if (err) return onError(err)
      ipc.send('wt-file-modtimes', torrent.key, fileModtimes)
    })
  }
}

// Produces a JSON saveable summary of a torrent
function getTorrentInfo (torrent) {
  return {
    infoHash: torrent.infoHash,
    magnetURI: torrent.magnetURI,
    name: torrent.name,
    path: torrent.path,
    files: torrent.files.map(getTorrentFileInfo),
    bytesReceived: torrent.received
  }
}

// Produces a JSON saveable summary of a file in a torrent
function getTorrentFileInfo (file) {
  return {
    name: file.name,
    length: file.length,
    path: file.path
  }
}

// Every time we resolve a magnet URI, save the torrent file so that we never
// have to download it again. Never ask the DHT the same question twice.
function saveTorrentFile (torrentKey) {
  const torrent = getTorrent(torrentKey)
  checkIfTorrentFileExists(torrent.infoHash, function (torrentPath, exists) {
    const fileName = torrent.infoHash + '.torrent'
    if (exists) {
      // We've already saved the file
      return ipc.send('wt-file-saved', torrentKey, fileName)
    }

    // Otherwise, save the .torrent file, under the app config folder
    fs.mkdir(config.TORRENT_PATH, function (_) {
      fs.writeFile(torrentPath, torrent.torrentFile, function (err) {
        if (err) return console.log('error saving torrent file %s: %o', torrentPath, err)
        console.log('saved torrent file %s', torrentPath)
        return ipc.send('wt-file-saved', torrentKey, fileName)
      })
    })
  })
}

// Checks whether we've already resolved a given infohash to a torrent file
// Calls back with (torrentPath, exists). Logs, does not call back on error
function checkIfTorrentFileExists (infoHash, cb) {
  const torrentPath = path.join(config.TORRENT_PATH, infoHash + '.torrent')
  fs.exists(torrentPath, function (exists) {
    cb(torrentPath, exists)
  })
}

// Save a JPG that represents a torrent.
// Auto chooses either a frame from a video file, an image, etc
function generateTorrentPoster (torrentKey) {
  const torrent = getTorrent(torrentKey)
  torrentPoster(torrent, function (err, buf, extension) {
    if (err) return console.log('error generating poster: %o', err)
    // save it for next time
    fs.mkdirp(config.POSTER_PATH, function (err) {
      if (err) return console.log('error creating poster dir: %o', err)
      const posterFileName = torrent.infoHash + extension
      const posterFilePath = path.join(config.POSTER_PATH, posterFileName)
      fs.writeFile(posterFilePath, buf, function (err) {
        if (err) return console.log('error saving poster: %o', err)
        // show the poster
        ipc.send('wt-poster', torrentKey, posterFileName)
      })
    })
  })
}

function updateTorrentProgress () {
  const progress = getTorrentProgress()
  // TODO: diff torrent-by-torrent, not once for the whole update
  if (prevProgress && deepEqual(progress, prevProgress, {strict: true})) {
    return /* don't send heavy object if it hasn't changed */
  }
  ipc.send('wt-progress', progress)
  prevProgress = progress
}

function getTorrentProgress () {
  // First, track overall progress
  const progress = client.progress
  const hasActiveTorrents = client.torrents.some(function (torrent) {
    return torrent.progress !== 1
  })

  // Track progress for every file in each torrent
  // TODO: ideally this would be tracked by WebTorrent, which could do it
  // more efficiently than looping over torrent.bitfield
  const torrentProg = client.torrents.map(function (torrent) {
    const fileProg = torrent.files && torrent.files.map(function (file, index) {
      const numPieces = file._endPiece - file._startPiece + 1
      let numPiecesPresent = 0
      for (let piece = file._startPiece; piece <= file._endPiece; piece++) {
        if (torrent.bitfield.get(piece)) numPiecesPresent++
      }
      return {
        startPiece: file._startPiece,
        endPiece: file._endPiece,
        numPieces,
        numPiecesPresent
      }
    })
    return {
      torrentKey: torrent.key,
      ready: torrent.ready,
      progress: torrent.progress,
      downloaded: torrent.downloaded,
      downloadSpeed: torrent.downloadSpeed,
      uploadSpeed: torrent.uploadSpeed,
      numPeers: torrent.numPeers,
      length: torrent.length,
      bitfield: torrent.bitfield,
      files: fileProg
    }
  })

  return {
    torrents: torrentProg,
    progress,
    hasActiveTorrents
  }
}

function startServer (infoHash) {
  const torrent = client.get(infoHash)
  if (torrent.ready) startServerFromReadyTorrent(torrent)
  else torrent.once('ready', () => startServerFromReadyTorrent(torrent))
}

function startServerFromReadyTorrent (torrent, cb) {
  if (server) return

  // start the streaming torrent-to-http server
  server = torrent.createServer()
  server.listen(0, function () {
    const port = server.address().port
    const urlSuffix = ':' + port
    const info = {
      torrentKey: torrent.key,
      localURL: 'http://localhost' + urlSuffix,
      networkURL: 'http://' + networkAddress() + urlSuffix
    }

    ipc.send('wt-server-running', info)
    ipc.send('wt-server-' + torrent.infoHash, info)
  })
}

function stopServer () {
  if (!server) return
  server.destroy()
  server = null
}

function getAudioMetadata (infoHash, index) {
  const torrent = client.get(infoHash)
  const file = torrent.files[index]
  musicmetadata(file.createReadStream(), function (err, info) {
    if (err) return console.log('error getting audio metadata for ' + infoHash + ':' + index, err)
    const { artist, album, albumartist, title, year, track, disk, genre } = info
    const importantInfo = { artist, album, albumartist, title, year, track, disk, genre }
    console.log('got audio metadata for %s: %o', file.name, importantInfo)
    ipc.send('wt-audio-metadata', infoHash, index, importantInfo)
  })
}

function selectFiles (torrentOrInfoHash, selections) {
  // Get the torrent object
  let torrent
  if (typeof torrentOrInfoHash === 'string') {
    torrent = client.get(torrentOrInfoHash)
  } else {
    torrent = torrentOrInfoHash
  }
  if (!torrent) {
    throw new Error('selectFiles: missing torrent ' + torrentOrInfoHash)
  }

  // Selections not specified?
  // Load all files. We still need to replace the default whole-torrent
  // selection with individual selections for each file, so we can
  // select/deselect files later on
  if (!selections) {
    selections = torrent.files.map((x) => true)
  }

  // Selections specified incorrectly?
  if (selections.length !== torrent.files.length) {
    throw new Error('got ' + selections.length + ' file selections, ' +
      'but the torrent contains ' + torrent.files.length + ' files')
  }

  // Remove default selection (whole torrent)
  torrent.deselect(0, torrent.pieces.length - 1, false)

  // Add selections (individual files)
  for (let i = 0; i < selections.length; i++) {
    const file = torrent.files[i]
    if (selections[i]) {
      file.select()
    } else {
      console.log('deselecting file ' + i + ' of torrent ' + torrent.name)
      file.deselect()
    }
  }
}

// Gets a WebTorrent handle by torrentKey
// Throws an Error if we're not currently torrenting anything w/ that key
function getTorrent (torrentKey) {
  const ret = client.torrents.find((x) => x.key === torrentKey)
  if (!ret) throw new TorrentKeyNotFoundError(torrentKey)
  return ret
}

function onError (err) {
  console.log(err)
}

// TODO: remove this once the following bugs are fixed:
// https://bugs.chromium.org/p/chromium/issues/detail?id=490143
// https://github.com/electron/electron/issues/7212
window.testOfflineMode = function () {
  console.log('Test, going OFFLINE')
  client = window.client = new WebTorrent({
    peerId: PEER_ID,
    tracker: false,
    dht: false,
    webSeeds: false
  })
  listenToClientEvents()
}
<|endoftext|>