|
|
@ -4,7 +4,9 @@ var DataView = require('../DataView'); |
|
|
|
var util = require('../util'); |
|
|
|
var Point3d = require('./Point3d'); |
|
|
|
var Point2d = require('./Point2d'); |
|
|
|
var Camera = require('./Camera'); |
|
|
|
var Filter = require('./Filter'); |
|
|
|
var Slider = require('./Slider'); |
|
|
|
var StepNumber = require('./StepNumber'); |
|
|
|
|
|
|
|
/** |
|
|
@ -49,7 +51,7 @@ function Graph3d(container, data, options) { |
|
|
|
this.animationInterval = 1000; // milliseconds
|
|
|
|
this.animationPreload = false; |
|
|
|
|
|
|
|
this.camera = new Graph3d.Camera(); |
|
|
|
this.camera = new Camera(); |
|
|
|
this.eye = new Point3d(0, 0, -1); // TODO: set eye.z about 3/4 of the width of the window?
|
|
|
|
|
|
|
|
this.dataTable = null; // The original data table
|
|
|
@ -98,138 +100,6 @@ function Graph3d(container, data, options) { |
|
|
|
// Extend Graph3d with an Emitter mixin
|
|
|
|
Emitter(Graph3d.prototype); |
|
|
|
|
|
|
|
/** |
|
|
|
* @class Camera |
|
|
|
* The camera is mounted on a (virtual) camera arm. The camera arm can rotate |
|
|
|
* The camera is always looking in the direction of the origin of the arm. |
|
|
|
* This way, the camera always rotates around one fixed point, the location |
|
|
|
* of the camera arm. |
|
|
|
* |
|
|
|
* Documentation: |
|
|
|
* http://en.wikipedia.org/wiki/3D_projection
|
|
|
|
*/ |
|
|
|
Graph3d.Camera = function () { |
|
|
|
this.armLocation = new Point3d(); |
|
|
|
this.armRotation = {}; |
|
|
|
this.armRotation.horizontal = 0; |
|
|
|
this.armRotation.vertical = 0; |
|
|
|
this.armLength = 1.7; |
|
|
|
|
|
|
|
this.cameraLocation = new Point3d(); |
|
|
|
this.cameraRotation = new Point3d(0.5*Math.PI, 0, 0); |
|
|
|
|
|
|
|
this.calculateCameraOrientation(); |
|
|
|
}; |
|
|
|
|
|
|
|
/** |
|
|
|
* Set the location (origin) of the arm |
|
|
|
* @param {Number} x Normalized value of x |
|
|
|
* @param {Number} y Normalized value of y |
|
|
|
* @param {Number} z Normalized value of z |
|
|
|
*/ |
|
|
|
Graph3d.Camera.prototype.setArmLocation = function(x, y, z) { |
|
|
|
this.armLocation.x = x; |
|
|
|
this.armLocation.y = y; |
|
|
|
this.armLocation.z = z; |
|
|
|
|
|
|
|
this.calculateCameraOrientation(); |
|
|
|
}; |
|
|
|
|
|
|
|
/** |
|
|
|
* Set the rotation of the camera arm |
|
|
|
* @param {Number} horizontal The horizontal rotation, between 0 and 2*PI. |
|
|
|
* Optional, can be left undefined. |
|
|
|
* @param {Number} vertical The vertical rotation, between 0 and 0.5*PI |
|
|
|
* if vertical=0.5*PI, the graph is shown from the |
|
|
|
* top. Optional, can be left undefined. |
|
|
|
*/ |
|
|
|
Graph3d.Camera.prototype.setArmRotation = function(horizontal, vertical) { |
|
|
|
if (horizontal !== undefined) { |
|
|
|
this.armRotation.horizontal = horizontal; |
|
|
|
} |
|
|
|
|
|
|
|
if (vertical !== undefined) { |
|
|
|
this.armRotation.vertical = vertical; |
|
|
|
if (this.armRotation.vertical < 0) this.armRotation.vertical = 0; |
|
|
|
if (this.armRotation.vertical > 0.5*Math.PI) this.armRotation.vertical = 0.5*Math.PI; |
|
|
|
} |
|
|
|
|
|
|
|
if (horizontal !== undefined || vertical !== undefined) { |
|
|
|
this.calculateCameraOrientation(); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
/** |
|
|
|
* Retrieve the current arm rotation |
|
|
|
* @return {object} An object with parameters horizontal and vertical |
|
|
|
*/ |
|
|
|
Graph3d.Camera.prototype.getArmRotation = function() { |
|
|
|
var rot = {}; |
|
|
|
rot.horizontal = this.armRotation.horizontal; |
|
|
|
rot.vertical = this.armRotation.vertical; |
|
|
|
|
|
|
|
return rot; |
|
|
|
}; |
|
|
|
|
|
|
|
/** |
|
|
|
* Set the (normalized) length of the camera arm. |
|
|
|
* @param {Number} length A length between 0.71 and 5.0 |
|
|
|
*/ |
|
|
|
Graph3d.Camera.prototype.setArmLength = function(length) { |
|
|
|
if (length === undefined) |
|
|
|
return; |
|
|
|
|
|
|
|
this.armLength = length; |
|
|
|
|
|
|
|
// Radius must be larger than the corner of the graph,
|
|
|
|
// which has a distance of sqrt(0.5^2+0.5^2) = 0.71 from the center of the
|
|
|
|
// graph
|
|
|
|
if (this.armLength < 0.71) this.armLength = 0.71; |
|
|
|
if (this.armLength > 5.0) this.armLength = 5.0; |
|
|
|
|
|
|
|
this.calculateCameraOrientation(); |
|
|
|
}; |
|
|
|
|
|
|
|
/** |
|
|
|
* Retrieve the arm length |
|
|
|
* @return {Number} length |
|
|
|
*/ |
|
|
|
Graph3d.Camera.prototype.getArmLength = function() { |
|
|
|
return this.armLength; |
|
|
|
}; |
|
|
|
|
|
|
|
/** |
|
|
|
* Retrieve the camera location |
|
|
|
* @return {Point3d} cameraLocation |
|
|
|
*/ |
|
|
|
Graph3d.Camera.prototype.getCameraLocation = function() { |
|
|
|
return this.cameraLocation; |
|
|
|
}; |
|
|
|
|
|
|
|
/** |
|
|
|
* Retrieve the camera rotation |
|
|
|
* @return {Point3d} cameraRotation |
|
|
|
*/ |
|
|
|
Graph3d.Camera.prototype.getCameraRotation = function() { |
|
|
|
return this.cameraRotation; |
|
|
|
}; |
|
|
|
|
|
|
|
/** |
|
|
|
* Calculate the location and rotation of the camera based on the |
|
|
|
* position and orientation of the camera arm |
|
|
|
*/ |
|
|
|
Graph3d.Camera.prototype.calculateCameraOrientation = function() { |
|
|
|
// calculate location of the camera
|
|
|
|
this.cameraLocation.x = this.armLocation.x - this.armLength * Math.sin(this.armRotation.horizontal) * Math.cos(this.armRotation.vertical); |
|
|
|
this.cameraLocation.y = this.armLocation.y - this.armLength * Math.cos(this.armRotation.horizontal) * Math.cos(this.armRotation.vertical); |
|
|
|
this.cameraLocation.z = this.armLocation.z + this.armLength * Math.sin(this.armRotation.vertical); |
|
|
|
|
|
|
|
// calculate rotation of the camera
|
|
|
|
this.cameraRotation.x = Math.PI/2 - this.armRotation.vertical; |
|
|
|
this.cameraRotation.y = 0; |
|
|
|
this.cameraRotation.z = -this.armRotation.horizontal; |
|
|
|
}; |
|
|
|
|
|
|
|
/** |
|
|
|
* Calculate the scaling values, dependent on the range in x, y, and z direction |
|
|
|
*/ |
|
|
@ -2372,351 +2242,6 @@ Graph3d.prototype._hideTooltip = function () { |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
/** |
|
|
|
* @constructor Slider |
|
|
|
* |
|
|
|
* An html slider control with start/stop/prev/next buttons |
|
|
|
* @param {Element} container The element where the slider will be created |
|
|
|
* @param {Object} options Available options: |
|
|
|
* {boolean} visible If true (default) the |
|
|
|
* slider is visible. |
|
|
|
*/ |
|
|
|
function Slider(container, options) { |
|
|
|
if (container === undefined) { |
|
|
|
throw 'Error: No container element defined'; |
|
|
|
} |
|
|
|
this.container = container; |
|
|
|
this.visible = (options && options.visible != undefined) ? options.visible : true; |
|
|
|
|
|
|
|
if (this.visible) { |
|
|
|
this.frame = document.createElement('DIV'); |
|
|
|
//this.frame.style.backgroundColor = '#E5E5E5';
|
|
|
|
this.frame.style.width = '100%'; |
|
|
|
this.frame.style.position = 'relative'; |
|
|
|
this.container.appendChild(this.frame); |
|
|
|
|
|
|
|
this.frame.prev = document.createElement('INPUT'); |
|
|
|
this.frame.prev.type = 'BUTTON'; |
|
|
|
this.frame.prev.value = 'Prev'; |
|
|
|
this.frame.appendChild(this.frame.prev); |
|
|
|
|
|
|
|
this.frame.play = document.createElement('INPUT'); |
|
|
|
this.frame.play.type = 'BUTTON'; |
|
|
|
this.frame.play.value = 'Play'; |
|
|
|
this.frame.appendChild(this.frame.play); |
|
|
|
|
|
|
|
this.frame.next = document.createElement('INPUT'); |
|
|
|
this.frame.next.type = 'BUTTON'; |
|
|
|
this.frame.next.value = 'Next'; |
|
|
|
this.frame.appendChild(this.frame.next); |
|
|
|
|
|
|
|
this.frame.bar = document.createElement('INPUT'); |
|
|
|
this.frame.bar.type = 'BUTTON'; |
|
|
|
this.frame.bar.style.position = 'absolute'; |
|
|
|
this.frame.bar.style.border = '1px solid red'; |
|
|
|
this.frame.bar.style.width = '100px'; |
|
|
|
this.frame.bar.style.height = '6px'; |
|
|
|
this.frame.bar.style.borderRadius = '2px'; |
|
|
|
this.frame.bar.style.MozBorderRadius = '2px'; |
|
|
|
this.frame.bar.style.border = '1px solid #7F7F7F'; |
|
|
|
this.frame.bar.style.backgroundColor = '#E5E5E5'; |
|
|
|
this.frame.appendChild(this.frame.bar); |
|
|
|
|
|
|
|
this.frame.slide = document.createElement('INPUT'); |
|
|
|
this.frame.slide.type = 'BUTTON'; |
|
|
|
this.frame.slide.style.margin = '0px'; |
|
|
|
this.frame.slide.value = ' '; |
|
|
|
this.frame.slide.style.position = 'relative'; |
|
|
|
this.frame.slide.style.left = '-100px'; |
|
|
|
this.frame.appendChild(this.frame.slide); |
|
|
|
|
|
|
|
// create events
|
|
|
|
var me = this; |
|
|
|
this.frame.slide.onmousedown = function (event) {me._onMouseDown(event);}; |
|
|
|
this.frame.prev.onclick = function (event) {me.prev(event);}; |
|
|
|
this.frame.play.onclick = function (event) {me.togglePlay(event);}; |
|
|
|
this.frame.next.onclick = function (event) {me.next(event);}; |
|
|
|
} |
|
|
|
|
|
|
|
this.onChangeCallback = undefined; |
|
|
|
|
|
|
|
this.values = []; |
|
|
|
this.index = undefined; |
|
|
|
|
|
|
|
this.playTimeout = undefined; |
|
|
|
this.playInterval = 1000; // milliseconds
|
|
|
|
this.playLoop = true; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Select the previous index |
|
|
|
*/ |
|
|
|
Slider.prototype.prev = function() { |
|
|
|
var index = this.getIndex(); |
|
|
|
if (index > 0) { |
|
|
|
index--; |
|
|
|
this.setIndex(index); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
/** |
|
|
|
* Select the next index |
|
|
|
*/ |
|
|
|
Slider.prototype.next = function() { |
|
|
|
var index = this.getIndex(); |
|
|
|
if (index < this.values.length - 1) { |
|
|
|
index++; |
|
|
|
this.setIndex(index); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
/** |
|
|
|
* Select the next index |
|
|
|
*/ |
|
|
|
Slider.prototype.playNext = function() { |
|
|
|
var start = new Date(); |
|
|
|
|
|
|
|
var index = this.getIndex(); |
|
|
|
if (index < this.values.length - 1) { |
|
|
|
index++; |
|
|
|
this.setIndex(index); |
|
|
|
} |
|
|
|
else if (this.playLoop) { |
|
|
|
// jump to the start
|
|
|
|
index = 0; |
|
|
|
this.setIndex(index); |
|
|
|
} |
|
|
|
|
|
|
|
var end = new Date(); |
|
|
|
var diff = (end - start); |
|
|
|
|
|
|
|
// calculate how much time it to to set the index and to execute the callback
|
|
|
|
// function.
|
|
|
|
var interval = Math.max(this.playInterval - diff, 0); |
|
|
|
// document.title = diff // TODO: cleanup
|
|
|
|
|
|
|
|
var me = this; |
|
|
|
this.playTimeout = setTimeout(function() {me.playNext();}, interval); |
|
|
|
}; |
|
|
|
|
|
|
|
/** |
|
|
|
* Toggle start or stop playing |
|
|
|
*/ |
|
|
|
Slider.prototype.togglePlay = function() { |
|
|
|
if (this.playTimeout === undefined) { |
|
|
|
this.play(); |
|
|
|
} else { |
|
|
|
this.stop(); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
/** |
|
|
|
* Start playing |
|
|
|
*/ |
|
|
|
Slider.prototype.play = function() { |
|
|
|
// Test whether already playing
|
|
|
|
if (this.playTimeout) return; |
|
|
|
|
|
|
|
this.playNext(); |
|
|
|
|
|
|
|
if (this.frame) { |
|
|
|
this.frame.play.value = 'Stop'; |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
/** |
|
|
|
* Stop playing |
|
|
|
*/ |
|
|
|
Slider.prototype.stop = function() { |
|
|
|
clearInterval(this.playTimeout); |
|
|
|
this.playTimeout = undefined; |
|
|
|
|
|
|
|
if (this.frame) { |
|
|
|
this.frame.play.value = 'Play'; |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
/** |
|
|
|
* Set a callback function which will be triggered when the value of the |
|
|
|
* slider bar has changed. |
|
|
|
*/ |
|
|
|
Slider.prototype.setOnChangeCallback = function(callback) { |
|
|
|
this.onChangeCallback = callback; |
|
|
|
}; |
|
|
|
|
|
|
|
/** |
|
|
|
* Set the interval for playing the list |
|
|
|
* @param {Number} interval The interval in milliseconds |
|
|
|
*/ |
|
|
|
Slider.prototype.setPlayInterval = function(interval) { |
|
|
|
this.playInterval = interval; |
|
|
|
}; |
|
|
|
|
|
|
|
/** |
|
|
|
* Retrieve the current play interval |
|
|
|
* @return {Number} interval The interval in milliseconds |
|
|
|
*/ |
|
|
|
Slider.prototype.getPlayInterval = function(interval) { |
|
|
|
return this.playInterval; |
|
|
|
}; |
|
|
|
|
|
|
|
/** |
|
|
|
* Set looping on or off |
|
|
|
* @pararm {boolean} doLoop If true, the slider will jump to the start when |
|
|
|
* the end is passed, and will jump to the end |
|
|
|
* when the start is passed. |
|
|
|
*/ |
|
|
|
Slider.prototype.setPlayLoop = function(doLoop) { |
|
|
|
this.playLoop = doLoop; |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* Execute the onchange callback function |
|
|
|
*/ |
|
|
|
Slider.prototype.onChange = function() { |
|
|
|
if (this.onChangeCallback !== undefined) { |
|
|
|
this.onChangeCallback(); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
/** |
|
|
|
* redraw the slider on the correct place |
|
|
|
*/ |
|
|
|
Slider.prototype.redraw = function() { |
|
|
|
if (this.frame) { |
|
|
|
// resize the bar
|
|
|
|
this.frame.bar.style.top = (this.frame.clientHeight/2 - |
|
|
|
this.frame.bar.offsetHeight/2) + 'px'; |
|
|
|
this.frame.bar.style.width = (this.frame.clientWidth - |
|
|
|
this.frame.prev.clientWidth - |
|
|
|
this.frame.play.clientWidth - |
|
|
|
this.frame.next.clientWidth - 30) + 'px'; |
|
|
|
|
|
|
|
// position the slider button
|
|
|
|
var left = this.indexToLeft(this.index); |
|
|
|
this.frame.slide.style.left = (left) + 'px'; |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* Set the list with values for the slider |
|
|
|
* @param {Array} values A javascript array with values (any type) |
|
|
|
*/ |
|
|
|
Slider.prototype.setValues = function(values) { |
|
|
|
this.values = values; |
|
|
|
|
|
|
|
if (this.values.length > 0) |
|
|
|
this.setIndex(0); |
|
|
|
else |
|
|
|
this.index = undefined; |
|
|
|
}; |
|
|
|
|
|
|
|
/** |
|
|
|
* Select a value by its index |
|
|
|
* @param {Number} index |
|
|
|
*/ |
|
|
|
Slider.prototype.setIndex = function(index) { |
|
|
|
if (index < this.values.length) { |
|
|
|
this.index = index; |
|
|
|
|
|
|
|
this.redraw(); |
|
|
|
this.onChange(); |
|
|
|
} |
|
|
|
else { |
|
|
|
throw 'Error: index out of range'; |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
/** |
|
|
|
* retrieve the index of the currently selected vaue |
|
|
|
* @return {Number} index |
|
|
|
*/ |
|
|
|
Slider.prototype.getIndex = function() { |
|
|
|
return this.index; |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* retrieve the currently selected value |
|
|
|
* @return {*} value |
|
|
|
*/ |
|
|
|
Slider.prototype.get = function() { |
|
|
|
return this.values[this.index]; |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
Slider.prototype._onMouseDown = function(event) { |
|
|
|
// only react on left mouse button down
|
|
|
|
var leftButtonDown = event.which ? (event.which === 1) : (event.button === 1); |
|
|
|
if (!leftButtonDown) return; |
|
|
|
|
|
|
|
this.startClientX = event.clientX; |
|
|
|
this.startSlideX = parseFloat(this.frame.slide.style.left); |
|
|
|
|
|
|
|
this.frame.style.cursor = 'move'; |
|
|
|
|
|
|
|
// add event listeners to handle moving the contents
|
|
|
|
// we store the function onmousemove and onmouseup in the graph, so we can
|
|
|
|
// remove the eventlisteners lateron in the function mouseUp()
|
|
|
|
var me = this; |
|
|
|
this.onmousemove = function (event) {me._onMouseMove(event);}; |
|
|
|
this.onmouseup = function (event) {me._onMouseUp(event);}; |
|
|
|
util.addEventListener(document, 'mousemove', this.onmousemove); |
|
|
|
util.addEventListener(document, 'mouseup', this.onmouseup); |
|
|
|
util.preventDefault(event); |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
Slider.prototype.leftToIndex = function (left) { |
|
|
|
var width = parseFloat(this.frame.bar.style.width) - |
|
|
|
this.frame.slide.clientWidth - 10; |
|
|
|
var x = left - 3; |
|
|
|
|
|
|
|
var index = Math.round(x / width * (this.values.length-1)); |
|
|
|
if (index < 0) index = 0; |
|
|
|
if (index > this.values.length-1) index = this.values.length-1; |
|
|
|
|
|
|
|
return index; |
|
|
|
}; |
|
|
|
|
|
|
|
Slider.prototype.indexToLeft = function (index) { |
|
|
|
var width = parseFloat(this.frame.bar.style.width) - |
|
|
|
this.frame.slide.clientWidth - 10; |
|
|
|
|
|
|
|
var x = index / (this.values.length-1) * width; |
|
|
|
var left = x + 3; |
|
|
|
|
|
|
|
return left; |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Slider.prototype._onMouseMove = function (event) { |
|
|
|
var diff = event.clientX - this.startClientX; |
|
|
|
var x = this.startSlideX + diff; |
|
|
|
|
|
|
|
var index = this.leftToIndex(x); |
|
|
|
|
|
|
|
this.setIndex(index); |
|
|
|
|
|
|
|
util.preventDefault(); |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
Slider.prototype._onMouseUp = function (event) { |
|
|
|
this.frame.style.cursor = 'auto'; |
|
|
|
|
|
|
|
// remove event listeners
|
|
|
|
util.removeEventListener(document, 'mousemove', this.onmousemove); |
|
|
|
util.removeEventListener(document, 'mouseup', this.onmouseup); |
|
|
|
|
|
|
|
util.preventDefault(); |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**--------------------------------------------------------------------------**/ |
|
|
|
|
|
|
|
|
|
|
|