@ -1,208 +0,0 @@ | |||||
/** | |||||
* Jake build script | |||||
*/ | |||||
var jake = require('jake'), | |||||
browserify = require('browserify'), | |||||
wrench = require('wrench'), | |||||
CleanCSS = require('clean-css'), | |||||
fs = require('fs'); | |||||
require('jake-utils'); | |||||
// constants | |||||
var DIST = './dist'; | |||||
var VIS = DIST + '/vis.js'; | |||||
var VIS_CSS = DIST + '/vis.css'; | |||||
var VIS_TMP = DIST + '/vis.js.tmp'; | |||||
var VIS_MIN = DIST + '/vis.min.js'; | |||||
var VIS_MIN_CSS = DIST + '/vis.min.css'; | |||||
/** | |||||
* default task | |||||
*/ | |||||
desc('Default task: build all libraries'); | |||||
task('default', ['build', 'minify'], function () { | |||||
console.log('done'); | |||||
}); | |||||
/** | |||||
* build the visualization library vis.js | |||||
*/ | |||||
desc('Build the visualization library vis.js'); | |||||
task('build', {async: true}, function () { | |||||
jake.mkdirP(DIST); | |||||
jake.mkdirP(DIST + '/img'); | |||||
// concatenate and stringify the css files | |||||
concat({ | |||||
src: [ | |||||
'./src/timeline/component/css/timeline.css', | |||||
'./src/timeline/component/css/panel.css', | |||||
'./src/timeline/component/css/labelset.css', | |||||
'./src/timeline/component/css/itemset.css', | |||||
'./src/timeline/component/css/item.css', | |||||
'./src/timeline/component/css/timeaxis.css', | |||||
'./src/timeline/component/css/currenttime.css', | |||||
'./src/timeline/component/css/customtime.css', | |||||
'./src/timeline/component/css/animation.css', | |||||
'./src/timeline/component/css/dataaxis.css', | |||||
'./src/timeline/component/css/pathStyles.css', | |||||
'./src/network/css/network-manipulation.css', | |||||
'./src/network/css/network-navigation.css' | |||||
], | |||||
dest: VIS_CSS, | |||||
separator: '\n' | |||||
}); | |||||
console.log('created ' + VIS_CSS); | |||||
// concatenate the script files | |||||
concat({ | |||||
dest: VIS_TMP, | |||||
src: [ | |||||
'./src/module/imports.js', | |||||
'./src/shim.js', | |||||
'./src/util.js', | |||||
'./src/DOMutil.js', | |||||
'./src/DataSet.js', | |||||
'./src/DataView.js', | |||||
'./src/timeline/component/GraphGroup.js', | |||||
'./src/timeline/component/Legend.js', | |||||
'./src/timeline/component/DataAxis.js', | |||||
'./src/timeline/component/LineGraph.js', | |||||
'./src/timeline/DataStep.js', | |||||
'./src/timeline/Stack.js', | |||||
'./src/timeline/TimeStep.js', | |||||
'./src/timeline/Range.js', | |||||
'./src/timeline/component/Component.js', | |||||
'./src/timeline/component/TimeAxis.js', | |||||
'./src/timeline/component/CurrentTime.js', | |||||
'./src/timeline/component/CustomTime.js', | |||||
'./src/timeline/component/ItemSet.js', | |||||
'./src/timeline/component/item/*.js', | |||||
'./src/timeline/component/Group.js', | |||||
'./src/timeline/Timeline.js', | |||||
'./src/timeline/Graph2d.js', | |||||
'./src/network/dotparser.js', | |||||
'./src/network/shapes.js', | |||||
'./src/network/Node.js', | |||||
'./src/network/Edge.js', | |||||
'./src/network/Popup.js', | |||||
'./src/network/Groups.js', | |||||
'./src/network/Images.js', | |||||
'./src/network/networkMixins/physics/PhysicsMixin.js', | |||||
'./src/network/networkMixins/physics/HierarchialRepulsion.js', | |||||
'./src/network/networkMixins/physics/BarnesHut.js', | |||||
'./src/network/networkMixins/physics/Repulsion.js', | |||||
'./src/network/networkMixins/HierarchicalLayoutMixin.js', | |||||
'./src/network/networkMixins/ManipulationMixin.js', | |||||
'./src/network/networkMixins/SectorsMixin.js', | |||||
'./src/network/networkMixins/ClusterMixin.js', | |||||
'./src/network/networkMixins/SelectionMixin.js', | |||||
'./src/network/networkMixins/NavigationMixin.js', | |||||
'./src/network/networkMixins/MixinLoader.js', | |||||
'./src/network/Network.js', | |||||
'./src/graph3d/Graph3d.js', | |||||
'./src/module/exports.js' | |||||
], | |||||
separator: '\n' | |||||
}); | |||||
// copy images | |||||
wrench.copyDirSyncRecursive('./src/network/img', DIST + '/img/network', { | |||||
forceDelete: true | |||||
}); | |||||
wrench.copyDirSyncRecursive('./src/timeline/img', DIST + '/img/timeline', { | |||||
forceDelete: true | |||||
}); | |||||
var timeStart = Date.now(); | |||||
// bundle the concatenated script and dependencies into one file | |||||
var b = browserify(); | |||||
b.add(VIS_TMP); | |||||
b.bundle({ | |||||
standalone: 'vis' | |||||
}, function (err, code) { | |||||
if(err) { | |||||
throw err; | |||||
} | |||||
console.log("browserify",Date.now() - timeStart); timeStart = Date.now(); | |||||
// add header and footer | |||||
var lib = read('./src/module/header.js') + code; | |||||
// write bundled file | |||||
write(VIS, lib); | |||||
console.log('created js' + VIS); | |||||
// remove temporary file | |||||
fs.unlinkSync(VIS_TMP); | |||||
// update version number and stuff in the javascript files | |||||
replacePlaceholders(VIS); | |||||
complete(); | |||||
}); | |||||
}); | |||||
/** | |||||
* minify the visualization library vis.js | |||||
*/ | |||||
desc('Minify the visualization library vis.js'); | |||||
task('minify', {async: true}, function () { | |||||
// minify javascript | |||||
minify({ | |||||
src: VIS, | |||||
dest: VIS_MIN, | |||||
header: read('./src/module/header.js') | |||||
}); | |||||
// update version number and stuff in the javascript files | |||||
replacePlaceholders(VIS_MIN); | |||||
console.log('created minified ' + VIS_MIN); | |||||
var minified = new CleanCSS().minify(read(VIS_CSS)); | |||||
write(VIS_MIN_CSS, minified); | |||||
console.log('created minified ' + VIS_MIN_CSS); | |||||
}); | |||||
/** | |||||
* test task | |||||
*/ | |||||
desc('Test the library'); | |||||
task('test', function () { | |||||
// TODO: use a testing suite for testing: nodeunit, mocha, tap, ... | |||||
var filelist = new jake.FileList(); | |||||
filelist.include([ | |||||
'./test/**/*.js' | |||||
]); | |||||
var files = filelist.toArray(); | |||||
files.forEach(function (file) { | |||||
require('./' + file); | |||||
}); | |||||
console.log('Executed ' + files.length + ' test files successfully'); | |||||
}); | |||||
/** | |||||
* replace version, date, and name placeholders in the provided file | |||||
* @param {String} filename | |||||
*/ | |||||
var replacePlaceholders = function (filename) { | |||||
replace({ | |||||
replacements: [ | |||||
{pattern: '@@date', replacement: today()}, | |||||
{pattern: '@@version', replacement: version()} | |||||
], | |||||
src: filename | |||||
}); | |||||
}; |
@ -0,0 +1,139 @@ | |||||
var fs = require('fs'); | |||||
var gulp = require('gulp'); | |||||
var gutil = require('gulp-util'); | |||||
var concat = require('gulp-concat'); | |||||
var minifyCSS = require('gulp-minify-css'); | |||||
var rename = require("gulp-rename"); | |||||
var webpack = require('webpack'); | |||||
var uglify = require('uglify-js'); | |||||
var rimraf = require('rimraf'); | |||||
var merge = require('merge-stream'); | |||||
var ENTRY = './index.js'; | |||||
var HEADER = './lib/header.js'; | |||||
var DIST = './dist'; | |||||
var VIS_JS = 'vis.js'; | |||||
var VIS_MAP = 'vis.map'; | |||||
var VIS_CSS = 'vis.css'; | |||||
var VIS_MIN_CSS = 'vis.min.css'; | |||||
var DIST_VIS_MIN_JS = DIST + '/vis.min.js'; | |||||
var DIST_VIS_MAP = DIST + '/' + VIS_MAP; | |||||
// generate banner with today's date and correct version | |||||
function createBanner() { | |||||
var today = gutil.date(new Date(), 'yyyy-mm-dd'); // today, formatted as yyyy-mm-dd | |||||
var version = require('./package.json').version; | |||||
return String(fs.readFileSync(HEADER)) | |||||
.replace('@@date', today) | |||||
.replace('@@version', version); | |||||
} | |||||
var bannerPlugin = new webpack.BannerPlugin(createBanner(), { | |||||
entryOnly: true, | |||||
raw: true | |||||
}); | |||||
// TODO: the moment.js language files should be excluded by default (they are quite big) | |||||
var webpackConfig = { | |||||
entry: ENTRY, | |||||
output: { | |||||
library: 'vis', | |||||
libraryTarget: 'umd', | |||||
path: DIST, | |||||
filename: VIS_JS, | |||||
sourcePrefix: ' ' | |||||
}, | |||||
plugins: [ bannerPlugin ], | |||||
cache: true | |||||
}; | |||||
var uglifyConfig = { | |||||
outSourceMap: VIS_MAP, | |||||
output: { | |||||
comments: /@license/ | |||||
} | |||||
}; | |||||
// create a single instance of the compiler to allow caching | |||||
var compiler = webpack(webpackConfig); | |||||
// clean the dist directory | |||||
gulp.task('clean', function (cb) { | |||||
rimraf(DIST, cb); | |||||
}); | |||||
gulp.task('bundle-js', ['clean'], function (cb) { | |||||
// update the banner contents (has a date in it which should stay up to date) | |||||
bannerPlugin.banner = createBanner(); | |||||
compiler.run(function (err, stats) { | |||||
if (err) gutil.log(err); | |||||
cb(); | |||||
}); | |||||
}); | |||||
// bundle and minify css | |||||
gulp.task('bundle-css', ['clean'], function () { | |||||
var files = [ | |||||
'./lib/timeline/component/css/timeline.css', | |||||
'./lib/timeline/component/css/panel.css', | |||||
'./lib/timeline/component/css/labelset.css', | |||||
'./lib/timeline/component/css/itemset.css', | |||||
'./lib/timeline/component/css/item.css', | |||||
'./lib/timeline/component/css/timeaxis.css', | |||||
'./lib/timeline/component/css/currenttime.css', | |||||
'./lib/timeline/component/css/customtime.css', | |||||
'./lib/timeline/component/css/animation.css', | |||||
'./lib/timeline/component/css/dataaxis.css', | |||||
'./lib/timeline/component/css/pathStyles.css', | |||||
'./lib/network/css/network-manipulation.css', | |||||
'./lib/network/css/network-navigation.css' | |||||
]; | |||||
return gulp.src(files) | |||||
.pipe(concat(VIS_CSS)) | |||||
.pipe(gulp.dest(DIST)) | |||||
// TODO: nicer to put minifying css in a separate task? | |||||
.pipe(minifyCSS()) | |||||
.pipe(rename(VIS_MIN_CSS)) | |||||
.pipe(gulp.dest(DIST)); | |||||
}); | |||||
gulp.task('copy-img', ['clean'], function () { | |||||
var network = gulp.src('./lib/network/img/**/*') | |||||
.pipe(gulp.dest(DIST + '/img/network')); | |||||
var timeline = gulp.src('./lib/timeline/img/**/*') | |||||
.pipe(gulp.dest(DIST + '/img/timeline')); | |||||
return merge(network, timeline); | |||||
}); | |||||
gulp.task('minify', ['bundle-js'], function (cb) { | |||||
var result = uglify.minify([DIST + '/' + VIS_JS], uglifyConfig); | |||||
fs.writeFileSync(DIST_VIS_MIN_JS, result.code); | |||||
fs.writeFileSync(DIST_VIS_MAP, result.map); | |||||
cb(); | |||||
}); | |||||
gulp.task('bundle', ['bundle-js', 'bundle-css', 'copy-img']); | |||||
// The watch task (to automatically rebuild when the source code changes) | |||||
gulp.task('watch', ['bundle', 'minify'], function () { | |||||
gulp.watch(['index.js', 'lib/**/*'], ['bundle', 'minify']); | |||||
}); | |||||
// The watch task (to automatically rebuild when the source code changes) | |||||
// this watch only rebuilds vis.js, not vis.min.js | |||||
gulp.task('watch-dev', ['bundle'], function () { | |||||
gulp.watch(['index.js', 'lib/**/*'], ['bundle']); | |||||
}); | |||||
// The default task (called when you run `gulp`) | |||||
gulp.task('default', ['clean', 'bundle', 'minify']); |
@ -0,0 +1,56 @@ | |||||
// utils | |||||
exports.util = require('./lib/util'); | |||||
exports.DOMutil = require('./lib/DOMutil'); | |||||
// data | |||||
exports.DataSet = require('./lib/DataSet'); | |||||
exports.DataView = require('./lib/DataView'); | |||||
// Graph3d | |||||
exports.Graph3d = require('./lib/graph3d/Graph3d'); | |||||
// Timeline | |||||
exports.Timeline = require('./lib/timeline/Timeline'); | |||||
exports.Graph2d = require('./lib/timeline/Graph2d'); | |||||
exports.timeline= { | |||||
DataStep: require('./lib/timeline/DataStep'), | |||||
Range: require('./lib/timeline/Range'), | |||||
stack: require('./lib/timeline/Stack'), | |||||
TimeStep: require('./lib/timeline/TimeStep'), | |||||
components: { | |||||
items: { | |||||
Item: require('./lib/timeline/component/item/Item'), | |||||
ItemBox: require('./lib/timeline/component/item/ItemBox'), | |||||
ItemPoint: require('./lib/timeline/component/item/ItemPoint'), | |||||
ItemRange: require('./lib/timeline/component/item/ItemRange') | |||||
}, | |||||
Component: require('./lib/timeline/component/Component'), | |||||
CurrentTime: require('./lib/timeline/component/CurrentTime'), | |||||
CustomTime: require('./lib/timeline/component/CustomTime'), | |||||
DataAxis: require('./lib/timeline/component/DataAxis'), | |||||
GraphGroup: require('./lib/timeline/component/GraphGroup'), | |||||
Group: require('./lib/timeline/component/Group'), | |||||
ItemSet: require('./lib/timeline/component/ItemSet'), | |||||
Legend: require('./lib/timeline/component/Legend'), | |||||
LineGraph: require('./lib/timeline/component/LineGraph'), | |||||
TimeAxis: require('./lib/timeline/component/TimeAxis') | |||||
} | |||||
}; | |||||
// Network | |||||
exports.Network = require('./lib/network/Network'); | |||||
exports.network = { | |||||
Edge: require('./lib/network/Edge'), | |||||
Groups: require('./lib/network/Groups'), | |||||
Images: require('./lib/network/Images'), | |||||
Node: require('./lib/network/Node'), | |||||
Popup: require('./lib/network/Popup'), | |||||
dotparser: require('./lib/network/dotparser') | |||||
}; | |||||
// Deprecated since v3.0.0 | |||||
exports.Graph = function () { | |||||
throw new Error('Graph is renamed to Network. Please create a graph as new vis.Network(...)'); | |||||
}; |
@ -0,0 +1,218 @@ | |||||
var DataView = require('../DataView'); | |||||
/** | |||||
* @class Filter | |||||
* | |||||
* @param {DataSet} data The google data table | |||||
* @param {Number} column The index of the column to be filtered | |||||
* @param {Graph} graph The graph | |||||
*/ | |||||
function Filter (data, column, graph) { | |||||
this.data = data; | |||||
this.column = column; | |||||
this.graph = graph; // the parent graph | |||||
this.index = undefined; | |||||
this.value = undefined; | |||||
// read all distinct values and select the first one | |||||
this.values = graph.getDistinctValues(data.get(), this.column); | |||||
// sort both numeric and string values correctly | |||||
this.values.sort(function (a, b) { | |||||
return a > b ? 1 : a < b ? -1 : 0; | |||||
}); | |||||
if (this.values.length > 0) { | |||||
this.selectValue(0); | |||||
} | |||||
// create an array with the filtered datapoints. this will be loaded afterwards | |||||
this.dataPoints = []; | |||||
this.loaded = false; | |||||
this.onLoadCallback = undefined; | |||||
if (graph.animationPreload) { | |||||
this.loaded = false; | |||||
this.loadInBackground(); | |||||
} | |||||
else { | |||||
this.loaded = true; | |||||
} | |||||
}; | |||||
/** | |||||
* Return the label | |||||
* @return {string} label | |||||
*/ | |||||
Filter.prototype.isLoaded = function() { | |||||
return this.loaded; | |||||
}; | |||||
/** | |||||
* Return the loaded progress | |||||
* @return {Number} percentage between 0 and 100 | |||||
*/ | |||||
Filter.prototype.getLoadedProgress = function() { | |||||
var len = this.values.length; | |||||
var i = 0; | |||||
while (this.dataPoints[i]) { | |||||
i++; | |||||
} | |||||
return Math.round(i / len * 100); | |||||
}; | |||||
/** | |||||
* Return the label | |||||
* @return {string} label | |||||
*/ | |||||
Filter.prototype.getLabel = function() { | |||||
return this.graph.filterLabel; | |||||
}; | |||||
/** | |||||
* Return the columnIndex of the filter | |||||
* @return {Number} columnIndex | |||||
*/ | |||||
Filter.prototype.getColumn = function() { | |||||
return this.column; | |||||
}; | |||||
/** | |||||
* Return the currently selected value. Returns undefined if there is no selection | |||||
* @return {*} value | |||||
*/ | |||||
Filter.prototype.getSelectedValue = function() { | |||||
if (this.index === undefined) | |||||
return undefined; | |||||
return this.values[this.index]; | |||||
}; | |||||
/** | |||||
* Retrieve all values of the filter | |||||
* @return {Array} values | |||||
*/ | |||||
Filter.prototype.getValues = function() { | |||||
return this.values; | |||||
}; | |||||
/** | |||||
* Retrieve one value of the filter | |||||
* @param {Number} index | |||||
* @return {*} value | |||||
*/ | |||||
Filter.prototype.getValue = function(index) { | |||||
if (index >= this.values.length) | |||||
throw 'Error: index out of range'; | |||||
return this.values[index]; | |||||
}; | |||||
/** | |||||
* Retrieve the (filtered) dataPoints for the currently selected filter index | |||||
* @param {Number} [index] (optional) | |||||
* @return {Array} dataPoints | |||||
*/ | |||||
Filter.prototype._getDataPoints = function(index) { | |||||
if (index === undefined) | |||||
index = this.index; | |||||
if (index === undefined) | |||||
return []; | |||||
var dataPoints; | |||||
if (this.dataPoints[index]) { | |||||
dataPoints = this.dataPoints[index]; | |||||
} | |||||
else { | |||||
var f = {}; | |||||
f.column = this.column; | |||||
f.value = this.values[index]; | |||||
var dataView = new DataView(this.data,{filter: function (item) {return (item[f.column] == f.value);}}).get(); | |||||
dataPoints = this.graph._getDataPoints(dataView); | |||||
this.dataPoints[index] = dataPoints; | |||||
} | |||||
return dataPoints; | |||||
}; | |||||
/** | |||||
* Set a callback function when the filter is fully loaded. | |||||
*/ | |||||
Filter.prototype.setOnLoadCallback = function(callback) { | |||||
this.onLoadCallback = callback; | |||||
}; | |||||
/** | |||||
* Add a value to the list with available values for this filter | |||||
* No double entries will be created. | |||||
* @param {Number} index | |||||
*/ | |||||
Filter.prototype.selectValue = function(index) { | |||||
if (index >= this.values.length) | |||||
throw 'Error: index out of range'; | |||||
this.index = index; | |||||
this.value = this.values[index]; | |||||
}; | |||||
/** | |||||
* Load all filtered rows in the background one by one | |||||
* Start this method without providing an index! | |||||
*/ | |||||
Filter.prototype.loadInBackground = function(index) { | |||||
if (index === undefined) | |||||
index = 0; | |||||
var frame = this.graph.frame; | |||||
if (index < this.values.length) { | |||||
var dataPointsTemp = this._getDataPoints(index); | |||||
//this.graph.redrawInfo(); // TODO: not neat | |||||
// create a progress box | |||||
if (frame.progress === undefined) { | |||||
frame.progress = document.createElement('DIV'); | |||||
frame.progress.style.position = 'absolute'; | |||||
frame.progress.style.color = 'gray'; | |||||
frame.appendChild(frame.progress); | |||||
} | |||||
var progress = this.getLoadedProgress(); | |||||
frame.progress.innerHTML = 'Loading animation... ' + progress + '%'; | |||||
// TODO: this is no nice solution... | |||||
frame.progress.style.bottom = 60 + 'px'; // TODO: use height of slider | |||||
frame.progress.style.left = 10 + 'px'; | |||||
var me = this; | |||||
setTimeout(function() {me.loadInBackground(index+1);}, 10); | |||||
this.loaded = false; | |||||
} | |||||
else { | |||||
this.loaded = true; | |||||
// remove the progress box | |||||
if (frame.progress !== undefined) { | |||||
frame.removeChild(frame.progress); | |||||
frame.progress = undefined; | |||||
} | |||||
if (this.onLoadCallback) | |||||
this.onLoadCallback(); | |||||
} | |||||
}; | |||||
module.exports = Filter; |
@ -0,0 +1,11 @@ | |||||
/** | |||||
* @prototype Point2d | |||||
* @param {Number} [x] | |||||
* @param {Number} [y] | |||||
*/ | |||||
Point2d = function (x, y) { | |||||
this.x = x !== undefined ? x : 0; | |||||
this.y = y !== undefined ? y : 0; | |||||
}; | |||||
module.exports = Point2d; |
@ -0,0 +1,85 @@ | |||||
/** | |||||
* @prototype Point3d | |||||
* @param {Number} [x] | |||||
* @param {Number} [y] | |||||
* @param {Number} [z] | |||||
*/ | |||||
function Point3d(x, y, z) { | |||||
this.x = x !== undefined ? x : 0; | |||||
this.y = y !== undefined ? y : 0; | |||||
this.z = z !== undefined ? z : 0; | |||||
}; | |||||
/** | |||||
* Subtract the two provided points, returns a-b | |||||
* @param {Point3d} a | |||||
* @param {Point3d} b | |||||
* @return {Point3d} a-b | |||||
*/ | |||||
Point3d.subtract = function(a, b) { | |||||
var sub = new Point3d(); | |||||
sub.x = a.x - b.x; | |||||
sub.y = a.y - b.y; | |||||
sub.z = a.z - b.z; | |||||
return sub; | |||||
}; | |||||
/** | |||||
* Add the two provided points, returns a+b | |||||
* @param {Point3d} a | |||||
* @param {Point3d} b | |||||
* @return {Point3d} a+b | |||||
*/ | |||||
Point3d.add = function(a, b) { | |||||
var sum = new Point3d(); | |||||
sum.x = a.x + b.x; | |||||
sum.y = a.y + b.y; | |||||
sum.z = a.z + b.z; | |||||
return sum; | |||||
}; | |||||
/** | |||||
* Calculate the average of two 3d points | |||||
* @param {Point3d} a | |||||
* @param {Point3d} b | |||||
* @return {Point3d} The average, (a+b)/2 | |||||
*/ | |||||
Point3d.avg = function(a, b) { | |||||
return new Point3d( | |||||
(a.x + b.x) / 2, | |||||
(a.y + b.y) / 2, | |||||
(a.z + b.z) / 2 | |||||
); | |||||
}; | |||||
/** | |||||
* Calculate the cross product of the two provided points, returns axb | |||||
* Documentation: http://en.wikipedia.org/wiki/Cross_product | |||||
* @param {Point3d} a | |||||
* @param {Point3d} b | |||||
* @return {Point3d} cross product axb | |||||
*/ | |||||
Point3d.crossProduct = function(a, b) { | |||||
var crossproduct = new Point3d(); | |||||
crossproduct.x = a.y * b.z - a.z * b.y; | |||||
crossproduct.y = a.z * b.x - a.x * b.z; | |||||
crossproduct.z = a.x * b.y - a.y * b.x; | |||||
return crossproduct; | |||||
}; | |||||
/** | |||||
* Rtrieve the length of the vector (or the distance from this point to the origin | |||||
* @return {Number} length | |||||
*/ | |||||
Point3d.prototype.length = function() { | |||||
return Math.sqrt( | |||||
this.x * this.x + | |||||
this.y * this.y + | |||||
this.z * this.z | |||||
); | |||||
}; | |||||
module.exports = Point3d; |
@ -0,0 +1,140 @@ | |||||
/** | |||||
* @prototype StepNumber | |||||
* The class StepNumber is an iterator for Numbers. You provide a start and end | |||||
* value, and a best step size. StepNumber itself rounds to fixed values and | |||||
* a finds the step that best fits the provided step. | |||||
* | |||||
* If prettyStep is true, the step size is chosen as close as possible to the | |||||
* provided step, but being a round value like 1, 2, 5, 10, 20, 50, .... | |||||
* | |||||
* Example usage: | |||||
* var step = new StepNumber(0, 10, 2.5, true); | |||||
* step.start(); | |||||
* while (!step.end()) { | |||||
* alert(step.getCurrent()); | |||||
* step.next(); | |||||
* } | |||||
* | |||||
* Version: 1.0 | |||||
* | |||||
* @param {Number} start The start value | |||||
* @param {Number} end The end value | |||||
* @param {Number} step Optional. Step size. Must be a positive value. | |||||
* @param {boolean} prettyStep Optional. If true, the step size is rounded | |||||
* To a pretty step size (like 1, 2, 5, 10, 20, 50, ...) | |||||
*/ | |||||
function StepNumber(start, end, step, prettyStep) { | |||||
// set default values | |||||
this._start = 0; | |||||
this._end = 0; | |||||
this._step = 1; | |||||
this.prettyStep = true; | |||||
this.precision = 5; | |||||
this._current = 0; | |||||
this.setRange(start, end, step, prettyStep); | |||||
}; | |||||
/** | |||||
* Set a new range: start, end and step. | |||||
* | |||||
* @param {Number} start The start value | |||||
* @param {Number} end The end value | |||||
* @param {Number} step Optional. Step size. Must be a positive value. | |||||
* @param {boolean} prettyStep Optional. If true, the step size is rounded | |||||
* To a pretty step size (like 1, 2, 5, 10, 20, 50, ...) | |||||
*/ | |||||
StepNumber.prototype.setRange = function(start, end, step, prettyStep) { | |||||
this._start = start ? start : 0; | |||||
this._end = end ? end : 0; | |||||
this.setStep(step, prettyStep); | |||||
}; | |||||
/** | |||||
* Set a new step size | |||||
* @param {Number} step New step size. Must be a positive value | |||||
* @param {boolean} prettyStep Optional. If true, the provided step is rounded | |||||
* to a pretty step size (like 1, 2, 5, 10, 20, 50, ...) | |||||
*/ | |||||
StepNumber.prototype.setStep = function(step, prettyStep) { | |||||
if (step === undefined || step <= 0) | |||||
return; | |||||
if (prettyStep !== undefined) | |||||
this.prettyStep = prettyStep; | |||||
if (this.prettyStep === true) | |||||
this._step = StepNumber.calculatePrettyStep(step); | |||||
else | |||||
this._step = step; | |||||
}; | |||||
/** | |||||
* Calculate a nice step size, closest to the desired step size. | |||||
* Returns a value in one of the ranges 1*10^n, 2*10^n, or 5*10^n, where n is an | |||||
* integer Number. For example 1, 2, 5, 10, 20, 50, etc... | |||||
* @param {Number} step Desired step size | |||||
* @return {Number} Nice step size | |||||
*/ | |||||
StepNumber.calculatePrettyStep = function (step) { | |||||
var log10 = function (x) {return Math.log(x) / Math.LN10;}; | |||||
// try three steps (multiple of 1, 2, or 5 | |||||
var step1 = Math.pow(10, Math.round(log10(step))), | |||||
step2 = 2 * Math.pow(10, Math.round(log10(step / 2))), | |||||
step5 = 5 * Math.pow(10, Math.round(log10(step / 5))); | |||||
// choose the best step (closest to minimum step) | |||||
var prettyStep = step1; | |||||
if (Math.abs(step2 - step) <= Math.abs(prettyStep - step)) prettyStep = step2; | |||||
if (Math.abs(step5 - step) <= Math.abs(prettyStep - step)) prettyStep = step5; | |||||
// for safety | |||||
if (prettyStep <= 0) { | |||||
prettyStep = 1; | |||||
} | |||||
return prettyStep; | |||||
}; | |||||
/** | |||||
* returns the current value of the step | |||||
* @return {Number} current value | |||||
*/ | |||||
StepNumber.prototype.getCurrent = function () { | |||||
return parseFloat(this._current.toPrecision(this.precision)); | |||||
}; | |||||
/** | |||||
* returns the current step size | |||||
* @return {Number} current step size | |||||
*/ | |||||
StepNumber.prototype.getStep = function () { | |||||
return this._step; | |||||
}; | |||||
/** | |||||
* Set the current value to the largest value smaller than start, which | |||||
* is a multiple of the step size | |||||
*/ | |||||
StepNumber.prototype.start = function() { | |||||
this._current = this._start - this._start % this._step; | |||||
}; | |||||
/** | |||||
* Do a step, add the step size to the current value | |||||
*/ | |||||
StepNumber.prototype.next = function () { | |||||
this._current += this._step; | |||||
}; | |||||
/** | |||||
* Returns true whether the end is reached | |||||
* @return {boolean} True if the current value has passed the end value. | |||||
*/ | |||||
StepNumber.prototype.end = function () { | |||||
return (this._current > this._end); | |||||
}; | |||||
module.exports = StepNumber; |
@ -0,0 +1,10 @@ | |||||
// Only load hammer.js when in a browser environment | |||||
// (loading hammer.js in a node.js environment gives errors) | |||||
if (typeof window !== 'undefined') { | |||||
module.exports = require('hammerjs'); | |||||
} | |||||
else { | |||||
module.exports = function () { | |||||
throw Error('hammer.js is only available in a browser, not in node.js.'); | |||||
} | |||||
} |
@ -0,0 +1,3 @@ | |||||
// first check if moment.js is already loaded in the browser window, if so, | |||||
// use this instance. Else, load via commonjs. | |||||
module.exports = (typeof window !== 'undefined') && window['moment'] || require('moment'); |
@ -0,0 +1,826 @@ | |||||
/** | |||||
* Parse a text source containing data in DOT language into a JSON object. | |||||
* The object contains two lists: one with nodes and one with edges. | |||||
* | |||||
* DOT language reference: http://www.graphviz.org/doc/info/lang.html | |||||
* | |||||
* @param {String} data Text containing a graph in DOT-notation | |||||
* @return {Object} graph An object containing two parameters: | |||||
* {Object[]} nodes | |||||
* {Object[]} edges | |||||
*/ | |||||
function parseDOT (data) { | |||||
dot = data; | |||||
return parseGraph(); | |||||
} | |||||
// token types enumeration | |||||
var TOKENTYPE = { | |||||
NULL : 0, | |||||
DELIMITER : 1, | |||||
IDENTIFIER: 2, | |||||
UNKNOWN : 3 | |||||
}; | |||||
// map with all delimiters | |||||
var DELIMITERS = { | |||||
'{': true, | |||||
'}': true, | |||||
'[': true, | |||||
']': true, | |||||
';': true, | |||||
'=': true, | |||||
',': true, | |||||
'->': true, | |||||
'--': true | |||||
}; | |||||
var dot = ''; // current dot file | |||||
var index = 0; // current index in dot file | |||||
var c = ''; // current token character in expr | |||||
var token = ''; // current token | |||||
var tokenType = TOKENTYPE.NULL; // type of the token | |||||
/** | |||||
* Get the first character from the dot file. | |||||
* The character is stored into the char c. If the end of the dot file is | |||||
* reached, the function puts an empty string in c. | |||||
*/ | |||||
function first() { | |||||
index = 0; | |||||
c = dot.charAt(0); | |||||
} | |||||
/** | |||||
* Get the next character from the dot file. | |||||
* The character is stored into the char c. If the end of the dot file is | |||||
* reached, the function puts an empty string in c. | |||||
*/ | |||||
function next() { | |||||
index++; | |||||
c = dot.charAt(index); | |||||
} | |||||
/** | |||||
* Preview the next character from the dot file. | |||||
* @return {String} cNext | |||||
*/ | |||||
function nextPreview() { | |||||
return dot.charAt(index + 1); | |||||
} | |||||
/** | |||||
* Test whether given character is alphabetic or numeric | |||||
* @param {String} c | |||||
* @return {Boolean} isAlphaNumeric | |||||
*/ | |||||
var regexAlphaNumeric = /[a-zA-Z_0-9.:#]/; | |||||
function isAlphaNumeric(c) { | |||||
return regexAlphaNumeric.test(c); | |||||
} | |||||
/** | |||||
* Merge all properties of object b into object b | |||||
* @param {Object} a | |||||
* @param {Object} b | |||||
* @return {Object} a | |||||
*/ | |||||
function merge (a, b) { | |||||
if (!a) { | |||||
a = {}; | |||||
} | |||||
if (b) { | |||||
for (var name in b) { | |||||
if (b.hasOwnProperty(name)) { | |||||
a[name] = b[name]; | |||||
} | |||||
} | |||||
} | |||||
return a; | |||||
} | |||||
/** | |||||
* Set a value in an object, where the provided parameter name can be a | |||||
* path with nested parameters. For example: | |||||
* | |||||
* var obj = {a: 2}; | |||||
* setValue(obj, 'b.c', 3); // obj = {a: 2, b: {c: 3}} | |||||
* | |||||
* @param {Object} obj | |||||
* @param {String} path A parameter name or dot-separated parameter path, | |||||
* like "color.highlight.border". | |||||
* @param {*} value | |||||
*/ | |||||
function setValue(obj, path, value) { | |||||
var keys = path.split('.'); | |||||
var o = obj; | |||||
while (keys.length) { | |||||
var key = keys.shift(); | |||||
if (keys.length) { | |||||
// this isn't the end point | |||||
if (!o[key]) { | |||||
o[key] = {}; | |||||
} | |||||
o = o[key]; | |||||
} | |||||
else { | |||||
// this is the end point | |||||
o[key] = value; | |||||
} | |||||
} | |||||
} | |||||
/** | |||||
* Add a node to a graph object. If there is already a node with | |||||
* the same id, their attributes will be merged. | |||||
* @param {Object} graph | |||||
* @param {Object} node | |||||
*/ | |||||
function addNode(graph, node) { | |||||
var i, len; | |||||
var current = null; | |||||
// find root graph (in case of subgraph) | |||||
var graphs = [graph]; // list with all graphs from current graph to root graph | |||||
var root = graph; | |||||
while (root.parent) { | |||||
graphs.push(root.parent); | |||||
root = root.parent; | |||||
} | |||||
// find existing node (at root level) by its id | |||||
if (root.nodes) { | |||||
for (i = 0, len = root.nodes.length; i < len; i++) { | |||||
if (node.id === root.nodes[i].id) { | |||||
current = root.nodes[i]; | |||||
break; | |||||
} | |||||
} | |||||
} | |||||
if (!current) { | |||||
// this is a new node | |||||
current = { | |||||
id: node.id | |||||
}; | |||||
if (graph.node) { | |||||
// clone default attributes | |||||
current.attr = merge(current.attr, graph.node); | |||||
} | |||||
} | |||||
// add node to this (sub)graph and all its parent graphs | |||||
for (i = graphs.length - 1; i >= 0; i--) { | |||||
var g = graphs[i]; | |||||
if (!g.nodes) { | |||||
g.nodes = []; | |||||
} | |||||
if (g.nodes.indexOf(current) == -1) { | |||||
g.nodes.push(current); | |||||
} | |||||
} | |||||
// merge attributes | |||||
if (node.attr) { | |||||
current.attr = merge(current.attr, node.attr); | |||||
} | |||||
} | |||||
/** | |||||
* Add an edge to a graph object | |||||
* @param {Object} graph | |||||
* @param {Object} edge | |||||
*/ | |||||
function addEdge(graph, edge) { | |||||
if (!graph.edges) { | |||||
graph.edges = []; | |||||
} | |||||
graph.edges.push(edge); | |||||
if (graph.edge) { | |||||
var attr = merge({}, graph.edge); // clone default attributes | |||||
edge.attr = merge(attr, edge.attr); // merge attributes | |||||
} | |||||
} | |||||
/** | |||||
* Create an edge to a graph object | |||||
* @param {Object} graph | |||||
* @param {String | Number | Object} from | |||||
* @param {String | Number | Object} to | |||||
* @param {String} type | |||||
* @param {Object | null} attr | |||||
* @return {Object} edge | |||||
*/ | |||||
function createEdge(graph, from, to, type, attr) { | |||||
var edge = { | |||||
from: from, | |||||
to: to, | |||||
type: type | |||||
}; | |||||
if (graph.edge) { | |||||
edge.attr = merge({}, graph.edge); // clone default attributes | |||||
} | |||||
edge.attr = merge(edge.attr || {}, attr); // merge attributes | |||||
return edge; | |||||
} | |||||
/** | |||||
* Get next token in the current dot file. | |||||
* The token and token type are available as token and tokenType | |||||
*/ | |||||
function getToken() { | |||||
tokenType = TOKENTYPE.NULL; | |||||
token = ''; | |||||
// skip over whitespaces | |||||
while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter | |||||
next(); | |||||
} | |||||
do { | |||||
var isComment = false; | |||||
// skip comment | |||||
if (c == '#') { | |||||
// find the previous non-space character | |||||
var i = index - 1; | |||||
while (dot.charAt(i) == ' ' || dot.charAt(i) == '\t') { | |||||
i--; | |||||
} | |||||
if (dot.charAt(i) == '\n' || dot.charAt(i) == '') { | |||||
// the # is at the start of a line, this is indeed a line comment | |||||
while (c != '' && c != '\n') { | |||||
next(); | |||||
} | |||||
isComment = true; | |||||
} | |||||
} | |||||
if (c == '/' && nextPreview() == '/') { | |||||
// skip line comment | |||||
while (c != '' && c != '\n') { | |||||
next(); | |||||
} | |||||
isComment = true; | |||||
} | |||||
if (c == '/' && nextPreview() == '*') { | |||||
// skip block comment | |||||
while (c != '') { | |||||
if (c == '*' && nextPreview() == '/') { | |||||
// end of block comment found. skip these last two characters | |||||
next(); | |||||
next(); | |||||
break; | |||||
} | |||||
else { | |||||
next(); | |||||
} | |||||
} | |||||
isComment = true; | |||||
} | |||||
// skip over whitespaces | |||||
while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter | |||||
next(); | |||||
} | |||||
} | |||||
while (isComment); | |||||
// check for end of dot file | |||||
if (c == '') { | |||||
// token is still empty | |||||
tokenType = TOKENTYPE.DELIMITER; | |||||
return; | |||||
} | |||||
// check for delimiters consisting of 2 characters | |||||
var c2 = c + nextPreview(); | |||||
if (DELIMITERS[c2]) { | |||||
tokenType = TOKENTYPE.DELIMITER; | |||||
token = c2; | |||||
next(); | |||||
next(); | |||||
return; | |||||
} | |||||
// check for delimiters consisting of 1 character | |||||
if (DELIMITERS[c]) { | |||||
tokenType = TOKENTYPE.DELIMITER; | |||||
token = c; | |||||
next(); | |||||
return; | |||||
} | |||||
// check for an identifier (number or string) | |||||
// TODO: more precise parsing of numbers/strings (and the port separator ':') | |||||
if (isAlphaNumeric(c) || c == '-') { | |||||
token += c; | |||||
next(); | |||||
while (isAlphaNumeric(c)) { | |||||
token += c; | |||||
next(); | |||||
} | |||||
if (token == 'false') { | |||||
token = false; // convert to boolean | |||||
} | |||||
else if (token == 'true') { | |||||
token = true; // convert to boolean | |||||
} | |||||
else if (!isNaN(Number(token))) { | |||||
token = Number(token); // convert to number | |||||
} | |||||
tokenType = TOKENTYPE.IDENTIFIER; | |||||
return; | |||||
} | |||||
// check for a string enclosed by double quotes | |||||
if (c == '"') { | |||||
next(); | |||||
while (c != '' && (c != '"' || (c == '"' && nextPreview() == '"'))) { | |||||
token += c; | |||||
if (c == '"') { // skip the escape character | |||||
next(); | |||||
} | |||||
next(); | |||||
} | |||||
if (c != '"') { | |||||
throw newSyntaxError('End of string " expected'); | |||||
} | |||||
next(); | |||||
tokenType = TOKENTYPE.IDENTIFIER; | |||||
return; | |||||
} | |||||
// something unknown is found, wrong characters, a syntax error | |||||
tokenType = TOKENTYPE.UNKNOWN; | |||||
while (c != '') { | |||||
token += c; | |||||
next(); | |||||
} | |||||
throw new SyntaxError('Syntax error in part "' + chop(token, 30) + '"'); | |||||
} | |||||
/** | |||||
* Parse a graph. | |||||
* @returns {Object} graph | |||||
*/ | |||||
function parseGraph() { | |||||
var graph = {}; | |||||
first(); | |||||
getToken(); | |||||
// optional strict keyword | |||||
if (token == 'strict') { | |||||
graph.strict = true; | |||||
getToken(); | |||||
} | |||||
// graph or digraph keyword | |||||
if (token == 'graph' || token == 'digraph') { | |||||
graph.type = token; | |||||
getToken(); | |||||
} | |||||
// optional graph id | |||||
if (tokenType == TOKENTYPE.IDENTIFIER) { | |||||
graph.id = token; | |||||
getToken(); | |||||
} | |||||
// open angle bracket | |||||
if (token != '{') { | |||||
throw newSyntaxError('Angle bracket { expected'); | |||||
} | |||||
getToken(); | |||||
// statements | |||||
parseStatements(graph); | |||||
// close angle bracket | |||||
if (token != '}') { | |||||
throw newSyntaxError('Angle bracket } expected'); | |||||
} | |||||
getToken(); | |||||
// end of file | |||||
if (token !== '') { | |||||
throw newSyntaxError('End of file expected'); | |||||
} | |||||
getToken(); | |||||
// remove temporary default properties | |||||
delete graph.node; | |||||
delete graph.edge; | |||||
delete graph.graph; | |||||
return graph; | |||||
} | |||||
/** | |||||
* Parse a list with statements. | |||||
* @param {Object} graph | |||||
*/ | |||||
function parseStatements (graph) { | |||||
while (token !== '' && token != '}') { | |||||
parseStatement(graph); | |||||
if (token == ';') { | |||||
getToken(); | |||||
} | |||||
} | |||||
} | |||||
/** | |||||
* Parse a single statement. Can be a an attribute statement, node | |||||
* statement, a series of node statements and edge statements, or a | |||||
* parameter. | |||||
* @param {Object} graph | |||||
*/ | |||||
function parseStatement(graph) { | |||||
// parse subgraph | |||||
var subgraph = parseSubgraph(graph); | |||||
if (subgraph) { | |||||
// edge statements | |||||
parseEdge(graph, subgraph); | |||||
return; | |||||
} | |||||
// parse an attribute statement | |||||
var attr = parseAttributeStatement(graph); | |||||
if (attr) { | |||||
return; | |||||
} | |||||
// parse node | |||||
if (tokenType != TOKENTYPE.IDENTIFIER) { | |||||
throw newSyntaxError('Identifier expected'); | |||||
} | |||||
var id = token; // id can be a string or a number | |||||
getToken(); | |||||
if (token == '=') { | |||||
// id statement | |||||
getToken(); | |||||
if (tokenType != TOKENTYPE.IDENTIFIER) { | |||||
throw newSyntaxError('Identifier expected'); | |||||
} | |||||
graph[id] = token; | |||||
getToken(); | |||||
// TODO: implement comma separated list with "a_list: ID=ID [','] [a_list] " | |||||
} | |||||
else { | |||||
parseNodeStatement(graph, id); | |||||
} | |||||
} | |||||
/** | |||||
* Parse a subgraph | |||||
* @param {Object} graph parent graph object | |||||
* @return {Object | null} subgraph | |||||
*/ | |||||
function parseSubgraph (graph) { | |||||
var subgraph = null; | |||||
// optional subgraph keyword | |||||
if (token == 'subgraph') { | |||||
subgraph = {}; | |||||
subgraph.type = 'subgraph'; | |||||
getToken(); | |||||
// optional graph id | |||||
if (tokenType == TOKENTYPE.IDENTIFIER) { | |||||
subgraph.id = token; | |||||
getToken(); | |||||
} | |||||
} | |||||
// open angle bracket | |||||
if (token == '{') { | |||||
getToken(); | |||||
if (!subgraph) { | |||||
subgraph = {}; | |||||
} | |||||
subgraph.parent = graph; | |||||
subgraph.node = graph.node; | |||||
subgraph.edge = graph.edge; | |||||
subgraph.graph = graph.graph; | |||||
// statements | |||||
parseStatements(subgraph); | |||||
// close angle bracket | |||||
if (token != '}') { | |||||
throw newSyntaxError('Angle bracket } expected'); | |||||
} | |||||
getToken(); | |||||
// remove temporary default properties | |||||
delete subgraph.node; | |||||
delete subgraph.edge; | |||||
delete subgraph.graph; | |||||
delete subgraph.parent; | |||||
// register at the parent graph | |||||
if (!graph.subgraphs) { | |||||
graph.subgraphs = []; | |||||
} | |||||
graph.subgraphs.push(subgraph); | |||||
} | |||||
return subgraph; | |||||
} | |||||
/** | |||||
* parse an attribute statement like "node [shape=circle fontSize=16]". | |||||
* Available keywords are 'node', 'edge', 'graph'. | |||||
* The previous list with default attributes will be replaced | |||||
* @param {Object} graph | |||||
* @returns {String | null} keyword Returns the name of the parsed attribute | |||||
* (node, edge, graph), or null if nothing | |||||
* is parsed. | |||||
*/ | |||||
function parseAttributeStatement (graph) { | |||||
// attribute statements | |||||
if (token == 'node') { | |||||
getToken(); | |||||
// node attributes | |||||
graph.node = parseAttributeList(); | |||||
return 'node'; | |||||
} | |||||
else if (token == 'edge') { | |||||
getToken(); | |||||
// edge attributes | |||||
graph.edge = parseAttributeList(); | |||||
return 'edge'; | |||||
} | |||||
else if (token == 'graph') { | |||||
getToken(); | |||||
// graph attributes | |||||
graph.graph = parseAttributeList(); | |||||
return 'graph'; | |||||
} | |||||
return null; | |||||
} | |||||
/** | |||||
* parse a node statement | |||||
* @param {Object} graph | |||||
* @param {String | Number} id | |||||
*/ | |||||
function parseNodeStatement(graph, id) { | |||||
// node statement | |||||
var node = { | |||||
id: id | |||||
}; | |||||
var attr = parseAttributeList(); | |||||
if (attr) { | |||||
node.attr = attr; | |||||
} | |||||
addNode(graph, node); | |||||
// edge statements | |||||
parseEdge(graph, id); | |||||
} | |||||
/** | |||||
* Parse an edge or a series of edges | |||||
* @param {Object} graph | |||||
* @param {String | Number} from Id of the from node | |||||
*/ | |||||
function parseEdge(graph, from) { | |||||
while (token == '->' || token == '--') { | |||||
var to; | |||||
var type = token; | |||||
getToken(); | |||||
var subgraph = parseSubgraph(graph); | |||||
if (subgraph) { | |||||
to = subgraph; | |||||
} | |||||
else { | |||||
if (tokenType != TOKENTYPE.IDENTIFIER) { | |||||
throw newSyntaxError('Identifier or subgraph expected'); | |||||
} | |||||
to = token; | |||||
addNode(graph, { | |||||
id: to | |||||
}); | |||||
getToken(); | |||||
} | |||||
// parse edge attributes | |||||
var attr = parseAttributeList(); | |||||
// create edge | |||||
var edge = createEdge(graph, from, to, type, attr); | |||||
addEdge(graph, edge); | |||||
from = to; | |||||
} | |||||
} | |||||
/** | |||||
* Parse a set with attributes, | |||||
* for example [label="1.000", shape=solid] | |||||
* @return {Object | null} attr | |||||
*/ | |||||
function parseAttributeList() { | |||||
var attr = null; | |||||
while (token == '[') { | |||||
getToken(); | |||||
attr = {}; | |||||
while (token !== '' && token != ']') { | |||||
if (tokenType != TOKENTYPE.IDENTIFIER) { | |||||
throw newSyntaxError('Attribute name expected'); | |||||
} | |||||
var name = token; | |||||
getToken(); | |||||
if (token != '=') { | |||||
throw newSyntaxError('Equal sign = expected'); | |||||
} | |||||
getToken(); | |||||
if (tokenType != TOKENTYPE.IDENTIFIER) { | |||||
throw newSyntaxError('Attribute value expected'); | |||||
} | |||||
var value = token; | |||||
setValue(attr, name, value); // name can be a path | |||||
getToken(); | |||||
if (token ==',') { | |||||
getToken(); | |||||
} | |||||
} | |||||
if (token != ']') { | |||||
throw newSyntaxError('Bracket ] expected'); | |||||
} | |||||
getToken(); | |||||
} | |||||
return attr; | |||||
} | |||||
/** | |||||
* Create a syntax error with extra information on current token and index. | |||||
* @param {String} message | |||||
* @returns {SyntaxError} err | |||||
*/ | |||||
function newSyntaxError(message) { | |||||
return new SyntaxError(message + ', got "' + chop(token, 30) + '" (char ' + index + ')'); | |||||
} | |||||
/** | |||||
* Chop off text after a maximum length | |||||
* @param {String} text | |||||
* @param {Number} maxLength | |||||
* @returns {String} | |||||
*/ | |||||
function chop (text, maxLength) { | |||||
return (text.length <= maxLength) ? text : (text.substr(0, 27) + '...'); | |||||
} | |||||
/** | |||||
* Execute a function fn for each pair of elements in two arrays | |||||
* @param {Array | *} array1 | |||||
* @param {Array | *} array2 | |||||
* @param {function} fn | |||||
*/ | |||||
function forEach2(array1, array2, fn) { | |||||
if (array1 instanceof Array) { | |||||
array1.forEach(function (elem1) { | |||||
if (array2 instanceof Array) { | |||||
array2.forEach(function (elem2) { | |||||
fn(elem1, elem2); | |||||
}); | |||||
} | |||||
else { | |||||
fn(elem1, array2); | |||||
} | |||||
}); | |||||
} | |||||
else { | |||||
if (array2 instanceof Array) { | |||||
array2.forEach(function (elem2) { | |||||
fn(array1, elem2); | |||||
}); | |||||
} | |||||
else { | |||||
fn(array1, array2); | |||||
} | |||||
} | |||||
} | |||||
/** | |||||
* Convert a string containing a graph in DOT language into a map containing | |||||
* with nodes and edges in the format of graph. | |||||
* @param {String} data Text containing a graph in DOT-notation | |||||
* @return {Object} graphData | |||||
*/ | |||||
function DOTToGraph (data) { | |||||
// parse the DOT file | |||||
var dotData = parseDOT(data); | |||||
var graphData = { | |||||
nodes: [], | |||||
edges: [], | |||||
options: {} | |||||
}; | |||||
// copy the nodes | |||||
if (dotData.nodes) { | |||||
dotData.nodes.forEach(function (dotNode) { | |||||
var graphNode = { | |||||
id: dotNode.id, | |||||
label: String(dotNode.label || dotNode.id) | |||||
}; | |||||
merge(graphNode, dotNode.attr); | |||||
if (graphNode.image) { | |||||
graphNode.shape = 'image'; | |||||
} | |||||
graphData.nodes.push(graphNode); | |||||
}); | |||||
} | |||||
// copy the edges | |||||
if (dotData.edges) { | |||||
/** | |||||
* Convert an edge in DOT format to an edge with VisGraph format | |||||
* @param {Object} dotEdge | |||||
* @returns {Object} graphEdge | |||||
*/ | |||||
function convertEdge(dotEdge) { | |||||
var graphEdge = { | |||||
from: dotEdge.from, | |||||
to: dotEdge.to | |||||
}; | |||||
merge(graphEdge, dotEdge.attr); | |||||
graphEdge.style = (dotEdge.type == '->') ? 'arrow' : 'line'; | |||||
return graphEdge; | |||||
} | |||||
dotData.edges.forEach(function (dotEdge) { | |||||
var from, to; | |||||
if (dotEdge.from instanceof Object) { | |||||
from = dotEdge.from.nodes; | |||||
} | |||||
else { | |||||
from = { | |||||
id: dotEdge.from | |||||
} | |||||
} | |||||
if (dotEdge.to instanceof Object) { | |||||
to = dotEdge.to.nodes; | |||||
} | |||||
else { | |||||
to = { | |||||
id: dotEdge.to | |||||
} | |||||
} | |||||
if (dotEdge.from instanceof Object && dotEdge.from.edges) { | |||||
dotEdge.from.edges.forEach(function (subEdge) { | |||||
var graphEdge = convertEdge(subEdge); | |||||
graphData.edges.push(graphEdge); | |||||
}); | |||||
} | |||||
forEach2(from, to, function (from, to) { | |||||
var subEdge = createEdge(graphData, from.id, to.id, dotEdge.type, dotEdge.attr); | |||||
var graphEdge = convertEdge(subEdge); | |||||
graphData.edges.push(graphEdge); | |||||
}); | |||||
if (dotEdge.to instanceof Object && dotEdge.to.edges) { | |||||
dotEdge.to.edges.forEach(function (subEdge) { | |||||
var graphEdge = convertEdge(subEdge); | |||||
graphData.edges.push(graphEdge); | |||||
}); | |||||
} | |||||
}); | |||||
} | |||||
// copy the options | |||||
if (dotData.attr) { | |||||
graphData.options = dotData.attr; | |||||
} | |||||
return graphData; | |||||
} | |||||
// exports | |||||
exports.parseDOT = parseDOT; | |||||
exports.DOTToGraph = DOTToGraph; |
@ -0,0 +1,304 @@ | |||||
exports._resetLevels = function() { | |||||
for (var nodeId in this.nodes) { | |||||
if (this.nodes.hasOwnProperty(nodeId)) { | |||||
var node = this.nodes[nodeId]; | |||||
if (node.preassignedLevel == false) { | |||||
node.level = -1; | |||||
} | |||||
} | |||||
} | |||||
}; | |||||
/** | |||||
* This is the main function to layout the nodes in a hierarchical way. | |||||
* It checks if the node details are supplied correctly | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._setupHierarchicalLayout = function() { | |||||
if (this.constants.hierarchicalLayout.enabled == true && this.nodeIndices.length > 0) { | |||||
if (this.constants.hierarchicalLayout.direction == "RL" || this.constants.hierarchicalLayout.direction == "DU") { | |||||
this.constants.hierarchicalLayout.levelSeparation *= -1; | |||||
} | |||||
else { | |||||
this.constants.hierarchicalLayout.levelSeparation = Math.abs(this.constants.hierarchicalLayout.levelSeparation); | |||||
} | |||||
// get the size of the largest hubs and check if the user has defined a level for a node. | |||||
var hubsize = 0; | |||||
var node, nodeId; | |||||
var definedLevel = false; | |||||
var undefinedLevel = false; | |||||
for (nodeId in this.nodes) { | |||||
if (this.nodes.hasOwnProperty(nodeId)) { | |||||
node = this.nodes[nodeId]; | |||||
if (node.level != -1) { | |||||
definedLevel = true; | |||||
} | |||||
else { | |||||
undefinedLevel = true; | |||||
} | |||||
if (hubsize < node.edges.length) { | |||||
hubsize = node.edges.length; | |||||
} | |||||
} | |||||
} | |||||
// if the user defined some levels but not all, alert and run without hierarchical layout | |||||
if (undefinedLevel == true && definedLevel == true) { | |||||
alert("To use the hierarchical layout, nodes require either no predefined levels or levels have to be defined for all nodes."); | |||||
this.zoomExtent(true,this.constants.clustering.enabled); | |||||
if (!this.constants.clustering.enabled) { | |||||
this.start(); | |||||
} | |||||
} | |||||
else { | |||||
// setup the system to use hierarchical method. | |||||
this._changeConstants(); | |||||
// define levels if undefined by the users. Based on hubsize | |||||
if (undefinedLevel == true) { | |||||
this._determineLevels(hubsize); | |||||
} | |||||
// check the distribution of the nodes per level. | |||||
var distribution = this._getDistribution(); | |||||
// place the nodes on the canvas. This also stablilizes the system. | |||||
this._placeNodesByHierarchy(distribution); | |||||
// start the simulation. | |||||
this.start(); | |||||
} | |||||
} | |||||
}; | |||||
/** | |||||
* This function places the nodes on the canvas based on the hierarchial distribution. | |||||
* | |||||
* @param {Object} distribution | obtained by the function this._getDistribution() | |||||
* @private | |||||
*/ | |||||
exports._placeNodesByHierarchy = function(distribution) { | |||||
var nodeId, node; | |||||
// start placing all the level 0 nodes first. Then recursively position their branches. | |||||
for (nodeId in distribution[0].nodes) { | |||||
if (distribution[0].nodes.hasOwnProperty(nodeId)) { | |||||
node = distribution[0].nodes[nodeId]; | |||||
if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") { | |||||
if (node.xFixed) { | |||||
node.x = distribution[0].minPos; | |||||
node.xFixed = false; | |||||
distribution[0].minPos += distribution[0].nodeSpacing; | |||||
} | |||||
} | |||||
else { | |||||
if (node.yFixed) { | |||||
node.y = distribution[0].minPos; | |||||
node.yFixed = false; | |||||
distribution[0].minPos += distribution[0].nodeSpacing; | |||||
} | |||||
} | |||||
this._placeBranchNodes(node.edges,node.id,distribution,node.level); | |||||
} | |||||
} | |||||
// stabilize the system after positioning. This function calls zoomExtent. | |||||
this._stabilize(); | |||||
}; | |||||
/** | |||||
* This function get the distribution of levels based on hubsize | |||||
* | |||||
* @returns {Object} | |||||
* @private | |||||
*/ | |||||
exports._getDistribution = function() { | |||||
var distribution = {}; | |||||
var nodeId, node, level; | |||||
// we fix Y because the hierarchy is vertical, we fix X so we do not give a node an x position for a second time. | |||||
// the fix of X is removed after the x value has been set. | |||||
for (nodeId in this.nodes) { | |||||
if (this.nodes.hasOwnProperty(nodeId)) { | |||||
node = this.nodes[nodeId]; | |||||
node.xFixed = true; | |||||
node.yFixed = true; | |||||
if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") { | |||||
node.y = this.constants.hierarchicalLayout.levelSeparation*node.level; | |||||
} | |||||
else { | |||||
node.x = this.constants.hierarchicalLayout.levelSeparation*node.level; | |||||
} | |||||
if (!distribution.hasOwnProperty(node.level)) { | |||||
distribution[node.level] = {amount: 0, nodes: {}, minPos:0, nodeSpacing:0}; | |||||
} | |||||
distribution[node.level].amount += 1; | |||||
distribution[node.level].nodes[node.id] = node; | |||||
} | |||||
} | |||||
// determine the largest amount of nodes of all levels | |||||
var maxCount = 0; | |||||
for (level in distribution) { | |||||
if (distribution.hasOwnProperty(level)) { | |||||
if (maxCount < distribution[level].amount) { | |||||
maxCount = distribution[level].amount; | |||||
} | |||||
} | |||||
} | |||||
// set the initial position and spacing of each nodes accordingly | |||||
for (level in distribution) { | |||||
if (distribution.hasOwnProperty(level)) { | |||||
distribution[level].nodeSpacing = (maxCount + 1) * this.constants.hierarchicalLayout.nodeSpacing; | |||||
distribution[level].nodeSpacing /= (distribution[level].amount + 1); | |||||
distribution[level].minPos = distribution[level].nodeSpacing - (0.5 * (distribution[level].amount + 1) * distribution[level].nodeSpacing); | |||||
} | |||||
} | |||||
return distribution; | |||||
}; | |||||
/** | |||||
* this function allocates nodes in levels based on the recursive branching from the largest hubs. | |||||
* | |||||
* @param hubsize | |||||
* @private | |||||
*/ | |||||
exports._determineLevels = function(hubsize) { | |||||
var nodeId, node; | |||||
// determine hubs | |||||
for (nodeId in this.nodes) { | |||||
if (this.nodes.hasOwnProperty(nodeId)) { | |||||
node = this.nodes[nodeId]; | |||||
if (node.edges.length == hubsize) { | |||||
node.level = 0; | |||||
} | |||||
} | |||||
} | |||||
// branch from hubs | |||||
for (nodeId in this.nodes) { | |||||
if (this.nodes.hasOwnProperty(nodeId)) { | |||||
node = this.nodes[nodeId]; | |||||
if (node.level == 0) { | |||||
this._setLevel(1,node.edges,node.id); | |||||
} | |||||
} | |||||
} | |||||
}; | |||||
/** | |||||
* Since hierarchical layout does not support: | |||||
* - smooth curves (based on the physics), | |||||
* - clustering (based on dynamic node counts) | |||||
* | |||||
* We disable both features so there will be no problems. | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._changeConstants = function() { | |||||
this.constants.clustering.enabled = false; | |||||
this.constants.physics.barnesHut.enabled = false; | |||||
this.constants.physics.hierarchicalRepulsion.enabled = true; | |||||
this._loadSelectedForceSolver(); | |||||
this.constants.smoothCurves = false; | |||||
this._configureSmoothCurves(); | |||||
}; | |||||
/** | |||||
* This is a recursively called function to enumerate the branches from the largest hubs and place the nodes | |||||
* on a X position that ensures there will be no overlap. | |||||
* | |||||
* @param edges | |||||
* @param parentId | |||||
* @param distribution | |||||
* @param parentLevel | |||||
* @private | |||||
*/ | |||||
exports._placeBranchNodes = function(edges, parentId, distribution, parentLevel) { | |||||
for (var i = 0; i < edges.length; i++) { | |||||
var childNode = null; | |||||
if (edges[i].toId == parentId) { | |||||
childNode = edges[i].from; | |||||
} | |||||
else { | |||||
childNode = edges[i].to; | |||||
} | |||||
// if a node is conneceted to another node on the same level (or higher (means lower level))!, this is not handled here. | |||||
var nodeMoved = false; | |||||
if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") { | |||||
if (childNode.xFixed && childNode.level > parentLevel) { | |||||
childNode.xFixed = false; | |||||
childNode.x = distribution[childNode.level].minPos; | |||||
nodeMoved = true; | |||||
} | |||||
} | |||||
else { | |||||
if (childNode.yFixed && childNode.level > parentLevel) { | |||||
childNode.yFixed = false; | |||||
childNode.y = distribution[childNode.level].minPos; | |||||
nodeMoved = true; | |||||
} | |||||
} | |||||
if (nodeMoved == true) { | |||||
distribution[childNode.level].minPos += distribution[childNode.level].nodeSpacing; | |||||
if (childNode.edges.length > 1) { | |||||
this._placeBranchNodes(childNode.edges,childNode.id,distribution,childNode.level); | |||||
} | |||||
} | |||||
} | |||||
}; | |||||
/** | |||||
* this function is called recursively to enumerate the barnches of the largest hubs and give each node a level. | |||||
* | |||||
* @param level | |||||
* @param edges | |||||
* @param parentId | |||||
* @private | |||||
*/ | |||||
exports._setLevel = function(level, edges, parentId) { | |||||
for (var i = 0; i < edges.length; i++) { | |||||
var childNode = null; | |||||
if (edges[i].toId == parentId) { | |||||
childNode = edges[i].from; | |||||
} | |||||
else { | |||||
childNode = edges[i].to; | |||||
} | |||||
if (childNode.level == -1 || childNode.level > level) { | |||||
childNode.level = level; | |||||
if (edges.length > 1) { | |||||
this._setLevel(level+1, childNode.edges, childNode.id); | |||||
} | |||||
} | |||||
} | |||||
}; | |||||
/** | |||||
* Unfix nodes | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._restoreNodes = function() { | |||||
for (var nodeId in this.nodes) { | |||||
if (this.nodes.hasOwnProperty(nodeId)) { | |||||
this.nodes[nodeId].xFixed = false; | |||||
this.nodes[nodeId].yFixed = false; | |||||
} | |||||
} | |||||
}; |
@ -0,0 +1,571 @@ | |||||
var util = require('../../util'); | |||||
/** | |||||
* clears the toolbar div element of children | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._clearManipulatorBar = function() { | |||||
while (this.manipulationDiv.hasChildNodes()) { | |||||
this.manipulationDiv.removeChild(this.manipulationDiv.firstChild); | |||||
} | |||||
}; | |||||
/** | |||||
* Manipulation UI temporarily overloads certain functions to extend or replace them. To be able to restore | |||||
* these functions to their original functionality, we saved them in this.cachedFunctions. | |||||
* This function restores these functions to their original function. | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._restoreOverloadedFunctions = function() { | |||||
for (var functionName in this.cachedFunctions) { | |||||
if (this.cachedFunctions.hasOwnProperty(functionName)) { | |||||
this[functionName] = this.cachedFunctions[functionName]; | |||||
} | |||||
} | |||||
}; | |||||
/** | |||||
* Enable or disable edit-mode. | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._toggleEditMode = function() { | |||||
this.editMode = !this.editMode; | |||||
var toolbar = document.getElementById("network-manipulationDiv"); | |||||
var closeDiv = document.getElementById("network-manipulation-closeDiv"); | |||||
var editModeDiv = document.getElementById("network-manipulation-editMode"); | |||||
if (this.editMode == true) { | |||||
toolbar.style.display="block"; | |||||
closeDiv.style.display="block"; | |||||
editModeDiv.style.display="none"; | |||||
closeDiv.onclick = this._toggleEditMode.bind(this); | |||||
} | |||||
else { | |||||
toolbar.style.display="none"; | |||||
closeDiv.style.display="none"; | |||||
editModeDiv.style.display="block"; | |||||
closeDiv.onclick = null; | |||||
} | |||||
this._createManipulatorBar() | |||||
}; | |||||
/** | |||||
* main function, creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar. | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._createManipulatorBar = function() { | |||||
// remove bound functions | |||||
if (this.boundFunction) { | |||||
this.off('select', this.boundFunction); | |||||
} | |||||
if (this.edgeBeingEdited !== undefined) { | |||||
this.edgeBeingEdited._disableControlNodes(); | |||||
this.edgeBeingEdited = undefined; | |||||
this.selectedControlNode = null; | |||||
} | |||||
// restore overloaded functions | |||||
this._restoreOverloadedFunctions(); | |||||
// resume calculation | |||||
this.freezeSimulation = false; | |||||
// reset global variables | |||||
this.blockConnectingEdgeSelection = false; | |||||
this.forceAppendSelection = false; | |||||
if (this.editMode == true) { | |||||
while (this.manipulationDiv.hasChildNodes()) { | |||||
this.manipulationDiv.removeChild(this.manipulationDiv.firstChild); | |||||
} | |||||
// add the icons to the manipulator div | |||||
this.manipulationDiv.innerHTML = "" + | |||||
"<span class='network-manipulationUI add' id='network-manipulate-addNode'>" + | |||||
"<span class='network-manipulationLabel'>"+this.constants.labels['add'] +"</span></span>" + | |||||
"<div class='network-seperatorLine'></div>" + | |||||
"<span class='network-manipulationUI connect' id='network-manipulate-connectNode'>" + | |||||
"<span class='network-manipulationLabel'>"+this.constants.labels['link'] +"</span></span>"; | |||||
if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) { | |||||
this.manipulationDiv.innerHTML += "" + | |||||
"<div class='network-seperatorLine'></div>" + | |||||
"<span class='network-manipulationUI edit' id='network-manipulate-editNode'>" + | |||||
"<span class='network-manipulationLabel'>"+this.constants.labels['editNode'] +"</span></span>"; | |||||
} | |||||
else if (this._getSelectedEdgeCount() == 1 && this._getSelectedNodeCount() == 0) { | |||||
this.manipulationDiv.innerHTML += "" + | |||||
"<div class='network-seperatorLine'></div>" + | |||||
"<span class='network-manipulationUI edit' id='network-manipulate-editEdge'>" + | |||||
"<span class='network-manipulationLabel'>"+this.constants.labels['editEdge'] +"</span></span>"; | |||||
} | |||||
if (this._selectionIsEmpty() == false) { | |||||
this.manipulationDiv.innerHTML += "" + | |||||
"<div class='network-seperatorLine'></div>" + | |||||
"<span class='network-manipulationUI delete' id='network-manipulate-delete'>" + | |||||
"<span class='network-manipulationLabel'>"+this.constants.labels['del'] +"</span></span>"; | |||||
} | |||||
// bind the icons | |||||
var addNodeButton = document.getElementById("network-manipulate-addNode"); | |||||
addNodeButton.onclick = this._createAddNodeToolbar.bind(this); | |||||
var addEdgeButton = document.getElementById("network-manipulate-connectNode"); | |||||
addEdgeButton.onclick = this._createAddEdgeToolbar.bind(this); | |||||
if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) { | |||||
var editButton = document.getElementById("network-manipulate-editNode"); | |||||
editButton.onclick = this._editNode.bind(this); | |||||
} | |||||
else if (this._getSelectedEdgeCount() == 1 && this._getSelectedNodeCount() == 0) { | |||||
var editButton = document.getElementById("network-manipulate-editEdge"); | |||||
editButton.onclick = this._createEditEdgeToolbar.bind(this); | |||||
} | |||||
if (this._selectionIsEmpty() == false) { | |||||
var deleteButton = document.getElementById("network-manipulate-delete"); | |||||
deleteButton.onclick = this._deleteSelected.bind(this); | |||||
} | |||||
var closeDiv = document.getElementById("network-manipulation-closeDiv"); | |||||
closeDiv.onclick = this._toggleEditMode.bind(this); | |||||
this.boundFunction = this._createManipulatorBar.bind(this); | |||||
this.on('select', this.boundFunction); | |||||
} | |||||
else { | |||||
this.editModeDiv.innerHTML = "" + | |||||
"<span class='network-manipulationUI edit editmode' id='network-manipulate-editModeButton'>" + | |||||
"<span class='network-manipulationLabel'>" + this.constants.labels['edit'] + "</span></span>"; | |||||
var editModeButton = document.getElementById("network-manipulate-editModeButton"); | |||||
editModeButton.onclick = this._toggleEditMode.bind(this); | |||||
} | |||||
}; | |||||
/** | |||||
* Create the toolbar for adding Nodes | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._createAddNodeToolbar = function() { | |||||
// clear the toolbar | |||||
this._clearManipulatorBar(); | |||||
if (this.boundFunction) { | |||||
this.off('select', this.boundFunction); | |||||
} | |||||
// create the toolbar contents | |||||
this.manipulationDiv.innerHTML = "" + | |||||
"<span class='network-manipulationUI back' id='network-manipulate-back'>" + | |||||
"<span class='network-manipulationLabel'>" + this.constants.labels['back'] + " </span></span>" + | |||||
"<div class='network-seperatorLine'></div>" + | |||||
"<span class='network-manipulationUI none' id='network-manipulate-back'>" + | |||||
"<span id='network-manipulatorLabel' class='network-manipulationLabel'>" + this.constants.labels['addDescription'] + "</span></span>"; | |||||
// bind the icon | |||||
var backButton = document.getElementById("network-manipulate-back"); | |||||
backButton.onclick = this._createManipulatorBar.bind(this); | |||||
// we use the boundFunction so we can reference it when we unbind it from the "select" event. | |||||
this.boundFunction = this._addNode.bind(this); | |||||
this.on('select', this.boundFunction); | |||||
}; | |||||
/** | |||||
* create the toolbar to connect nodes | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._createAddEdgeToolbar = function() { | |||||
// clear the toolbar | |||||
this._clearManipulatorBar(); | |||||
this._unselectAll(true); | |||||
this.freezeSimulation = true; | |||||
if (this.boundFunction) { | |||||
this.off('select', this.boundFunction); | |||||
} | |||||
this._unselectAll(); | |||||
this.forceAppendSelection = false; | |||||
this.blockConnectingEdgeSelection = true; | |||||
this.manipulationDiv.innerHTML = "" + | |||||
"<span class='network-manipulationUI back' id='network-manipulate-back'>" + | |||||
"<span class='network-manipulationLabel'>" + this.constants.labels['back'] + " </span></span>" + | |||||
"<div class='network-seperatorLine'></div>" + | |||||
"<span class='network-manipulationUI none' id='network-manipulate-back'>" + | |||||
"<span id='network-manipulatorLabel' class='network-manipulationLabel'>" + this.constants.labels['linkDescription'] + "</span></span>"; | |||||
// bind the icon | |||||
var backButton = document.getElementById("network-manipulate-back"); | |||||
backButton.onclick = this._createManipulatorBar.bind(this); | |||||
// we use the boundFunction so we can reference it when we unbind it from the "select" event. | |||||
this.boundFunction = this._handleConnect.bind(this); | |||||
this.on('select', this.boundFunction); | |||||
// temporarily overload functions | |||||
this.cachedFunctions["_handleTouch"] = this._handleTouch; | |||||
this.cachedFunctions["_handleOnRelease"] = this._handleOnRelease; | |||||
this._handleTouch = this._handleConnect; | |||||
this._handleOnRelease = this._finishConnect; | |||||
// redraw to show the unselect | |||||
this._redraw(); | |||||
}; | |||||
/** | |||||
* create the toolbar to edit edges | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._createEditEdgeToolbar = function() { | |||||
// clear the toolbar | |||||
this._clearManipulatorBar(); | |||||
if (this.boundFunction) { | |||||
this.off('select', this.boundFunction); | |||||
} | |||||
this.edgeBeingEdited = this._getSelectedEdge(); | |||||
this.edgeBeingEdited._enableControlNodes(); | |||||
this.manipulationDiv.innerHTML = "" + | |||||
"<span class='network-manipulationUI back' id='network-manipulate-back'>" + | |||||
"<span class='network-manipulationLabel'>" + this.constants.labels['back'] + " </span></span>" + | |||||
"<div class='network-seperatorLine'></div>" + | |||||
"<span class='network-manipulationUI none' id='network-manipulate-back'>" + | |||||
"<span id='network-manipulatorLabel' class='network-manipulationLabel'>" + this.constants.labels['editEdgeDescription'] + "</span></span>"; | |||||
// bind the icon | |||||
var backButton = document.getElementById("network-manipulate-back"); | |||||
backButton.onclick = this._createManipulatorBar.bind(this); | |||||
// temporarily overload functions | |||||
this.cachedFunctions["_handleTouch"] = this._handleTouch; | |||||
this.cachedFunctions["_handleOnRelease"] = this._handleOnRelease; | |||||
this.cachedFunctions["_handleTap"] = this._handleTap; | |||||
this.cachedFunctions["_handleDragStart"] = this._handleDragStart; | |||||
this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag; | |||||
this._handleTouch = this._selectControlNode; | |||||
this._handleTap = function () {}; | |||||
this._handleOnDrag = this._controlNodeDrag; | |||||
this._handleDragStart = function () {} | |||||
this._handleOnRelease = this._releaseControlNode; | |||||
// redraw to show the unselect | |||||
this._redraw(); | |||||
}; | |||||
/** | |||||
* the function bound to the selection event. It checks if you want to connect a cluster and changes the description | |||||
* to walk the user through the process. | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._selectControlNode = function(pointer) { | |||||
this.edgeBeingEdited.controlNodes.from.unselect(); | |||||
this.edgeBeingEdited.controlNodes.to.unselect(); | |||||
this.selectedControlNode = this.edgeBeingEdited._getSelectedControlNode(this._XconvertDOMtoCanvas(pointer.x),this._YconvertDOMtoCanvas(pointer.y)); | |||||
if (this.selectedControlNode !== null) { | |||||
this.selectedControlNode.select(); | |||||
this.freezeSimulation = true; | |||||
} | |||||
this._redraw(); | |||||
}; | |||||
/** | |||||
* the function bound to the selection event. It checks if you want to connect a cluster and changes the description | |||||
* to walk the user through the process. | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._controlNodeDrag = function(event) { | |||||
var pointer = this._getPointer(event.gesture.center); | |||||
if (this.selectedControlNode !== null && this.selectedControlNode !== undefined) { | |||||
this.selectedControlNode.x = this._XconvertDOMtoCanvas(pointer.x); | |||||
this.selectedControlNode.y = this._YconvertDOMtoCanvas(pointer.y); | |||||
} | |||||
this._redraw(); | |||||
}; | |||||
exports._releaseControlNode = function(pointer) { | |||||
var newNode = this._getNodeAt(pointer); | |||||
if (newNode != null) { | |||||
if (this.edgeBeingEdited.controlNodes.from.selected == true) { | |||||
this._editEdge(newNode.id, this.edgeBeingEdited.to.id); | |||||
this.edgeBeingEdited.controlNodes.from.unselect(); | |||||
} | |||||
if (this.edgeBeingEdited.controlNodes.to.selected == true) { | |||||
this._editEdge(this.edgeBeingEdited.from.id, newNode.id); | |||||
this.edgeBeingEdited.controlNodes.to.unselect(); | |||||
} | |||||
} | |||||
else { | |||||
this.edgeBeingEdited._restoreControlNodes(); | |||||
} | |||||
this.freezeSimulation = false; | |||||
this._redraw(); | |||||
}; | |||||
/** | |||||
* the function bound to the selection event. It checks if you want to connect a cluster and changes the description | |||||
* to walk the user through the process. | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._handleConnect = function(pointer) { | |||||
if (this._getSelectedNodeCount() == 0) { | |||||
var node = this._getNodeAt(pointer); | |||||
if (node != null) { | |||||
if (node.clusterSize > 1) { | |||||
alert("Cannot create edges to a cluster.") | |||||
} | |||||
else { | |||||
this._selectObject(node,false); | |||||
// create a node the temporary line can look at | |||||
this.sectors['support']['nodes']['targetNode'] = new Node({id:'targetNode'},{},{},this.constants); | |||||
this.sectors['support']['nodes']['targetNode'].x = node.x; | |||||
this.sectors['support']['nodes']['targetNode'].y = node.y; | |||||
this.sectors['support']['nodes']['targetViaNode'] = new Node({id:'targetViaNode'},{},{},this.constants); | |||||
this.sectors['support']['nodes']['targetViaNode'].x = node.x; | |||||
this.sectors['support']['nodes']['targetViaNode'].y = node.y; | |||||
this.sectors['support']['nodes']['targetViaNode'].parentEdgeId = "connectionEdge"; | |||||
// create a temporary edge | |||||
this.edges['connectionEdge'] = new Edge({id:"connectionEdge",from:node.id,to:this.sectors['support']['nodes']['targetNode'].id}, this, this.constants); | |||||
this.edges['connectionEdge'].from = node; | |||||
this.edges['connectionEdge'].connected = true; | |||||
this.edges['connectionEdge'].smooth = true; | |||||
this.edges['connectionEdge'].selected = true; | |||||
this.edges['connectionEdge'].to = this.sectors['support']['nodes']['targetNode']; | |||||
this.edges['connectionEdge'].via = this.sectors['support']['nodes']['targetViaNode']; | |||||
this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag; | |||||
this._handleOnDrag = function(event) { | |||||
var pointer = this._getPointer(event.gesture.center); | |||||
this.sectors['support']['nodes']['targetNode'].x = this._XconvertDOMtoCanvas(pointer.x); | |||||
this.sectors['support']['nodes']['targetNode'].y = this._YconvertDOMtoCanvas(pointer.y); | |||||
this.sectors['support']['nodes']['targetViaNode'].x = 0.5 * (this._XconvertDOMtoCanvas(pointer.x) + this.edges['connectionEdge'].from.x); | |||||
this.sectors['support']['nodes']['targetViaNode'].y = this._YconvertDOMtoCanvas(pointer.y); | |||||
}; | |||||
this.moving = true; | |||||
this.start(); | |||||
} | |||||
} | |||||
} | |||||
}; | |||||
exports._finishConnect = function(pointer) { | |||||
if (this._getSelectedNodeCount() == 1) { | |||||
// restore the drag function | |||||
this._handleOnDrag = this.cachedFunctions["_handleOnDrag"]; | |||||
delete this.cachedFunctions["_handleOnDrag"]; | |||||
// remember the edge id | |||||
var connectFromId = this.edges['connectionEdge'].fromId; | |||||
// remove the temporary nodes and edge | |||||
delete this.edges['connectionEdge']; | |||||
delete this.sectors['support']['nodes']['targetNode']; | |||||
delete this.sectors['support']['nodes']['targetViaNode']; | |||||
var node = this._getNodeAt(pointer); | |||||
if (node != null) { | |||||
if (node.clusterSize > 1) { | |||||
alert("Cannot create edges to a cluster.") | |||||
} | |||||
else { | |||||
this._createEdge(connectFromId,node.id); | |||||
this._createManipulatorBar(); | |||||
} | |||||
} | |||||
this._unselectAll(); | |||||
} | |||||
}; | |||||
/** | |||||
* Adds a node on the specified location | |||||
*/ | |||||
exports._addNode = function() { | |||||
if (this._selectionIsEmpty() && this.editMode == true) { | |||||
var positionObject = this._pointerToPositionObject(this.pointerPosition); | |||||
var defaultData = {id:util.randomUUID(),x:positionObject.left,y:positionObject.top,label:"new",allowedToMoveX:true,allowedToMoveY:true}; | |||||
if (this.triggerFunctions.add) { | |||||
if (this.triggerFunctions.add.length == 2) { | |||||
var me = this; | |||||
this.triggerFunctions.add(defaultData, function(finalizedData) { | |||||
me.nodesData.add(finalizedData); | |||||
me._createManipulatorBar(); | |||||
me.moving = true; | |||||
me.start(); | |||||
}); | |||||
} | |||||
else { | |||||
alert(this.constants.labels['addError']); | |||||
this._createManipulatorBar(); | |||||
this.moving = true; | |||||
this.start(); | |||||
} | |||||
} | |||||
else { | |||||
this.nodesData.add(defaultData); | |||||
this._createManipulatorBar(); | |||||
this.moving = true; | |||||
this.start(); | |||||
} | |||||
} | |||||
}; | |||||
/** | |||||
* connect two nodes with a new edge. | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._createEdge = function(sourceNodeId,targetNodeId) { | |||||
if (this.editMode == true) { | |||||
var defaultData = {from:sourceNodeId, to:targetNodeId}; | |||||
if (this.triggerFunctions.connect) { | |||||
if (this.triggerFunctions.connect.length == 2) { | |||||
var me = this; | |||||
this.triggerFunctions.connect(defaultData, function(finalizedData) { | |||||
me.edgesData.add(finalizedData); | |||||
me.moving = true; | |||||
me.start(); | |||||
}); | |||||
} | |||||
else { | |||||
alert(this.constants.labels["linkError"]); | |||||
this.moving = true; | |||||
this.start(); | |||||
} | |||||
} | |||||
else { | |||||
this.edgesData.add(defaultData); | |||||
this.moving = true; | |||||
this.start(); | |||||
} | |||||
} | |||||
}; | |||||
/** | |||||
* connect two nodes with a new edge. | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._editEdge = function(sourceNodeId,targetNodeId) { | |||||
if (this.editMode == true) { | |||||
var defaultData = {id: this.edgeBeingEdited.id, from:sourceNodeId, to:targetNodeId}; | |||||
if (this.triggerFunctions.editEdge) { | |||||
if (this.triggerFunctions.editEdge.length == 2) { | |||||
var me = this; | |||||
this.triggerFunctions.editEdge(defaultData, function(finalizedData) { | |||||
me.edgesData.update(finalizedData); | |||||
me.moving = true; | |||||
me.start(); | |||||
}); | |||||
} | |||||
else { | |||||
alert(this.constants.labels["linkError"]); | |||||
this.moving = true; | |||||
this.start(); | |||||
} | |||||
} | |||||
else { | |||||
this.edgesData.update(defaultData); | |||||
this.moving = true; | |||||
this.start(); | |||||
} | |||||
} | |||||
}; | |||||
/** | |||||
* Create the toolbar to edit the selected node. The label and the color can be changed. Other colors are derived from the chosen color. | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._editNode = function() { | |||||
if (this.triggerFunctions.edit && this.editMode == true) { | |||||
var node = this._getSelectedNode(); | |||||
var data = {id:node.id, | |||||
label: node.label, | |||||
group: node.group, | |||||
shape: node.shape, | |||||
color: { | |||||
background:node.color.background, | |||||
border:node.color.border, | |||||
highlight: { | |||||
background:node.color.highlight.background, | |||||
border:node.color.highlight.border | |||||
} | |||||
}}; | |||||
if (this.triggerFunctions.edit.length == 2) { | |||||
var me = this; | |||||
this.triggerFunctions.edit(data, function (finalizedData) { | |||||
me.nodesData.update(finalizedData); | |||||
me._createManipulatorBar(); | |||||
me.moving = true; | |||||
me.start(); | |||||
}); | |||||
} | |||||
else { | |||||
alert(this.constants.labels["editError"]); | |||||
} | |||||
} | |||||
else { | |||||
alert(this.constants.labels["editBoundError"]); | |||||
} | |||||
}; | |||||
/** | |||||
* delete everything in the selection | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._deleteSelected = function() { | |||||
if (!this._selectionIsEmpty() && this.editMode == true) { | |||||
if (!this._clusterInSelection()) { | |||||
var selectedNodes = this.getSelectedNodes(); | |||||
var selectedEdges = this.getSelectedEdges(); | |||||
if (this.triggerFunctions.del) { | |||||
var me = this; | |||||
var data = {nodes: selectedNodes, edges: selectedEdges}; | |||||
if (this.triggerFunctions.del.length = 2) { | |||||
this.triggerFunctions.del(data, function (finalizedData) { | |||||
me.edgesData.remove(finalizedData.edges); | |||||
me.nodesData.remove(finalizedData.nodes); | |||||
me._unselectAll(); | |||||
me.moving = true; | |||||
me.start(); | |||||
}); | |||||
} | |||||
else { | |||||
alert(this.constants.labels["deleteError"]) | |||||
} | |||||
} | |||||
else { | |||||
this.edgesData.remove(selectedEdges); | |||||
this.nodesData.remove(selectedNodes); | |||||
this._unselectAll(); | |||||
this.moving = true; | |||||
this.start(); | |||||
} | |||||
} | |||||
else { | |||||
alert(this.constants.labels["deleteClusterError"]); | |||||
} | |||||
} | |||||
}; |
@ -0,0 +1,198 @@ | |||||
var PhysicsMixin = require('./physics/PhysicsMixin'); | |||||
var ClusterMixin = require('./ClusterMixin'); | |||||
var SectorsMixin = require('./SectorsMixin'); | |||||
var SelectionMixin = require('./SelectionMixin'); | |||||
var ManipulationMixin = require('./ManipulationMixin'); | |||||
var NavigationMixin = require('./NavigationMixin'); | |||||
var HierarchicalLayoutMixin = require('./HierarchicalLayoutMixin'); | |||||
/** | |||||
* Load a mixin into the network object | |||||
* | |||||
* @param {Object} sourceVariable | this object has to contain functions. | |||||
* @private | |||||
*/ | |||||
exports._loadMixin = function (sourceVariable) { | |||||
for (var mixinFunction in sourceVariable) { | |||||
if (sourceVariable.hasOwnProperty(mixinFunction)) { | |||||
this[mixinFunction] = sourceVariable[mixinFunction]; | |||||
} | |||||
} | |||||
}; | |||||
/** | |||||
* removes a mixin from the network object. | |||||
* | |||||
* @param {Object} sourceVariable | this object has to contain functions. | |||||
* @private | |||||
*/ | |||||
exports._clearMixin = function (sourceVariable) { | |||||
for (var mixinFunction in sourceVariable) { | |||||
if (sourceVariable.hasOwnProperty(mixinFunction)) { | |||||
this[mixinFunction] = undefined; | |||||
} | |||||
} | |||||
}; | |||||
/** | |||||
* Mixin the physics system and initialize the parameters required. | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._loadPhysicsSystem = function () { | |||||
this._loadMixin(PhysicsMixin); | |||||
this._loadSelectedForceSolver(); | |||||
if (this.constants.configurePhysics == true) { | |||||
this._loadPhysicsConfiguration(); | |||||
} | |||||
}; | |||||
/** | |||||
* Mixin the cluster system and initialize the parameters required. | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._loadClusterSystem = function () { | |||||
this.clusterSession = 0; | |||||
this.hubThreshold = 5; | |||||
this._loadMixin(ClusterMixin); | |||||
}; | |||||
/** | |||||
* Mixin the sector system and initialize the parameters required | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._loadSectorSystem = function () { | |||||
this.sectors = {}; | |||||
this.activeSector = ["default"]; | |||||
this.sectors["active"] = {}; | |||||
this.sectors["active"]["default"] = {"nodes": {}, | |||||
"edges": {}, | |||||
"nodeIndices": [], | |||||
"formationScale": 1.0, | |||||
"drawingNode": undefined }; | |||||
this.sectors["frozen"] = {}; | |||||
this.sectors["support"] = {"nodes": {}, | |||||
"edges": {}, | |||||
"nodeIndices": [], | |||||
"formationScale": 1.0, | |||||
"drawingNode": undefined }; | |||||
this.nodeIndices = this.sectors["active"]["default"]["nodeIndices"]; // the node indices list is used to speed up the computation of the repulsion fields | |||||
this._loadMixin(SectorsMixin); | |||||
}; | |||||
/** | |||||
* Mixin the selection system and initialize the parameters required | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._loadSelectionSystem = function () { | |||||
this.selectionObj = {nodes: {}, edges: {}}; | |||||
this._loadMixin(SelectionMixin); | |||||
}; | |||||
/** | |||||
* Mixin the navigationUI (User Interface) system and initialize the parameters required | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._loadManipulationSystem = function () { | |||||
// reset global variables -- these are used by the selection of nodes and edges. | |||||
this.blockConnectingEdgeSelection = false; | |||||
this.forceAppendSelection = false; | |||||
if (this.constants.dataManipulation.enabled == true) { | |||||
// load the manipulator HTML elements. All styling done in css. | |||||
if (this.manipulationDiv === undefined) { | |||||
this.manipulationDiv = document.createElement('div'); | |||||
this.manipulationDiv.className = 'network-manipulationDiv'; | |||||
this.manipulationDiv.id = 'network-manipulationDiv'; | |||||
if (this.editMode == true) { | |||||
this.manipulationDiv.style.display = "block"; | |||||
} | |||||
else { | |||||
this.manipulationDiv.style.display = "none"; | |||||
} | |||||
this.containerElement.insertBefore(this.manipulationDiv, this.frame); | |||||
} | |||||
if (this.editModeDiv === undefined) { | |||||
this.editModeDiv = document.createElement('div'); | |||||
this.editModeDiv.className = 'network-manipulation-editMode'; | |||||
this.editModeDiv.id = 'network-manipulation-editMode'; | |||||
if (this.editMode == true) { | |||||
this.editModeDiv.style.display = "none"; | |||||
} | |||||
else { | |||||
this.editModeDiv.style.display = "block"; | |||||
} | |||||
this.containerElement.insertBefore(this.editModeDiv, this.frame); | |||||
} | |||||
if (this.closeDiv === undefined) { | |||||
this.closeDiv = document.createElement('div'); | |||||
this.closeDiv.className = 'network-manipulation-closeDiv'; | |||||
this.closeDiv.id = 'network-manipulation-closeDiv'; | |||||
this.closeDiv.style.display = this.manipulationDiv.style.display; | |||||
this.containerElement.insertBefore(this.closeDiv, this.frame); | |||||
} | |||||
// load the manipulation functions | |||||
this._loadMixin(ManipulationMixin); | |||||
// create the manipulator toolbar | |||||
this._createManipulatorBar(); | |||||
} | |||||
else { | |||||
if (this.manipulationDiv !== undefined) { | |||||
// removes all the bindings and overloads | |||||
this._createManipulatorBar(); | |||||
// remove the manipulation divs | |||||
this.containerElement.removeChild(this.manipulationDiv); | |||||
this.containerElement.removeChild(this.editModeDiv); | |||||
this.containerElement.removeChild(this.closeDiv); | |||||
this.manipulationDiv = undefined; | |||||
this.editModeDiv = undefined; | |||||
this.closeDiv = undefined; | |||||
// remove the mixin functions | |||||
this._clearMixin(ManipulationMixin); | |||||
} | |||||
} | |||||
}; | |||||
/** | |||||
* Mixin the navigation (User Interface) system and initialize the parameters required | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._loadNavigationControls = function () { | |||||
this._loadMixin(NavigationMixin); | |||||
// the clean function removes the button divs, this is done to remove the bindings. | |||||
this._cleanNavigation(); | |||||
if (this.constants.navigation.enabled == true) { | |||||
this._loadNavigationElements(); | |||||
} | |||||
}; | |||||
/** | |||||
* Mixin the hierarchical layout system. | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._loadHierarchySystem = function () { | |||||
this._loadMixin(HierarchicalLayoutMixin); | |||||
}; |
@ -0,0 +1,196 @@ | |||||
exports._cleanNavigation = function() { | |||||
// clean up previous navigation items | |||||
var wrapper = document.getElementById('network-navigation_wrapper'); | |||||
if (wrapper != null) { | |||||
this.containerElement.removeChild(wrapper); | |||||
} | |||||
document.onmouseup = null; | |||||
}; | |||||
/** | |||||
* Creation of the navigation controls nodes. They are drawn over the rest of the nodes and are not affected by scale and translation | |||||
* they have a triggerFunction which is called on click. If the position of the navigation controls is dependent | |||||
* on this.frame.canvas.clientWidth or this.frame.canvas.clientHeight, we flag horizontalAlignLeft and verticalAlignTop false. | |||||
* This means that the location will be corrected by the _relocateNavigation function on a size change of the canvas. | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._loadNavigationElements = function() { | |||||
this._cleanNavigation(); | |||||
this.navigationDivs = {}; | |||||
var navigationDivs = ['up','down','left','right','zoomIn','zoomOut','zoomExtends']; | |||||
var navigationDivActions = ['_moveUp','_moveDown','_moveLeft','_moveRight','_zoomIn','_zoomOut','zoomExtent']; | |||||
this.navigationDivs['wrapper'] = document.createElement('div'); | |||||
this.navigationDivs['wrapper'].id = "network-navigation_wrapper"; | |||||
this.navigationDivs['wrapper'].style.position = "absolute"; | |||||
this.navigationDivs['wrapper'].style.width = this.frame.canvas.clientWidth + "px"; | |||||
this.navigationDivs['wrapper'].style.height = this.frame.canvas.clientHeight + "px"; | |||||
this.containerElement.insertBefore(this.navigationDivs['wrapper'],this.frame); | |||||
for (var i = 0; i < navigationDivs.length; i++) { | |||||
this.navigationDivs[navigationDivs[i]] = document.createElement('div'); | |||||
this.navigationDivs[navigationDivs[i]].id = "network-navigation_" + navigationDivs[i]; | |||||
this.navigationDivs[navigationDivs[i]].className = "network-navigation " + navigationDivs[i]; | |||||
this.navigationDivs['wrapper'].appendChild(this.navigationDivs[navigationDivs[i]]); | |||||
this.navigationDivs[navigationDivs[i]].onmousedown = this[navigationDivActions[i]].bind(this); | |||||
} | |||||
document.onmouseup = this._stopMovement.bind(this); | |||||
}; | |||||
/** | |||||
* this stops all movement induced by the navigation buttons | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._stopMovement = function() { | |||||
this._xStopMoving(); | |||||
this._yStopMoving(); | |||||
this._stopZoom(); | |||||
}; | |||||
/** | |||||
* stops the actions performed by page up and down etc. | |||||
* | |||||
* @param event | |||||
* @private | |||||
*/ | |||||
exports._preventDefault = function(event) { | |||||
if (event !== undefined) { | |||||
if (event.preventDefault) { | |||||
event.preventDefault(); | |||||
} else { | |||||
event.returnValue = false; | |||||
} | |||||
} | |||||
}; | |||||
/** | |||||
* move the screen up | |||||
* By using the increments, instead of adding a fixed number to the translation, we keep fluent and | |||||
* instant movement. The onKeypress event triggers immediately, then pauses, then triggers frequently | |||||
* To avoid this behaviour, we do the translation in the start loop. | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._moveUp = function(event) { | |||||
this.yIncrement = this.constants.keyboard.speed.y; | |||||
this.start(); // if there is no node movement, the calculation wont be done | |||||
this._preventDefault(event); | |||||
if (this.navigationDivs) { | |||||
this.navigationDivs['up'].className += " active"; | |||||
} | |||||
}; | |||||
/** | |||||
* move the screen down | |||||
* @private | |||||
*/ | |||||
exports._moveDown = function(event) { | |||||
this.yIncrement = -this.constants.keyboard.speed.y; | |||||
this.start(); // if there is no node movement, the calculation wont be done | |||||
this._preventDefault(event); | |||||
if (this.navigationDivs) { | |||||
this.navigationDivs['down'].className += " active"; | |||||
} | |||||
}; | |||||
/** | |||||
* move the screen left | |||||
* @private | |||||
*/ | |||||
exports._moveLeft = function(event) { | |||||
this.xIncrement = this.constants.keyboard.speed.x; | |||||
this.start(); // if there is no node movement, the calculation wont be done | |||||
this._preventDefault(event); | |||||
if (this.navigationDivs) { | |||||
this.navigationDivs['left'].className += " active"; | |||||
} | |||||
}; | |||||
/** | |||||
* move the screen right | |||||
* @private | |||||
*/ | |||||
exports._moveRight = function(event) { | |||||
this.xIncrement = -this.constants.keyboard.speed.y; | |||||
this.start(); // if there is no node movement, the calculation wont be done | |||||
this._preventDefault(event); | |||||
if (this.navigationDivs) { | |||||
this.navigationDivs['right'].className += " active"; | |||||
} | |||||
}; | |||||
/** | |||||
* Zoom in, using the same method as the movement. | |||||
* @private | |||||
*/ | |||||
exports._zoomIn = function(event) { | |||||
this.zoomIncrement = this.constants.keyboard.speed.zoom; | |||||
this.start(); // if there is no node movement, the calculation wont be done | |||||
this._preventDefault(event); | |||||
if (this.navigationDivs) { | |||||
this.navigationDivs['zoomIn'].className += " active"; | |||||
} | |||||
}; | |||||
/** | |||||
* Zoom out | |||||
* @private | |||||
*/ | |||||
exports._zoomOut = function() { | |||||
this.zoomIncrement = -this.constants.keyboard.speed.zoom; | |||||
this.start(); // if there is no node movement, the calculation wont be done | |||||
this._preventDefault(event); | |||||
if (this.navigationDivs) { | |||||
this.navigationDivs['zoomOut'].className += " active"; | |||||
} | |||||
}; | |||||
/** | |||||
* Stop zooming and unhighlight the zoom controls | |||||
* @private | |||||
*/ | |||||
exports._stopZoom = function() { | |||||
this.zoomIncrement = 0; | |||||
if (this.navigationDivs) { | |||||
this.navigationDivs['zoomIn'].className = this.navigationDivs['zoomIn'].className.replace(" active",""); | |||||
this.navigationDivs['zoomOut'].className = this.navigationDivs['zoomOut'].className.replace(" active",""); | |||||
} | |||||
}; | |||||
/** | |||||
* Stop moving in the Y direction and unHighlight the up and down | |||||
* @private | |||||
*/ | |||||
exports._yStopMoving = function() { | |||||
this.yIncrement = 0; | |||||
if (this.navigationDivs) { | |||||
this.navigationDivs['up'].className = this.navigationDivs['up'].className.replace(" active",""); | |||||
this.navigationDivs['down'].className = this.navigationDivs['down'].className.replace(" active",""); | |||||
} | |||||
}; | |||||
/** | |||||
* Stop moving in the X direction and unHighlight left and right. | |||||
* @private | |||||
*/ | |||||
exports._xStopMoving = function() { | |||||
this.xIncrement = 0; | |||||
if (this.navigationDivs) { | |||||
this.navigationDivs['left'].className = this.navigationDivs['left'].className.replace(" active",""); | |||||
this.navigationDivs['right'].className = this.navigationDivs['right'].className.replace(" active",""); | |||||
} | |||||
}; |
@ -0,0 +1,548 @@ | |||||
var util = require('../../util'); | |||||
/** | |||||
* Creation of the SectorMixin var. | |||||
* | |||||
* This contains all the functions the Network object can use to employ the sector system. | |||||
* The sector system is always used by Network, though the benefits only apply to the use of clustering. | |||||
* If clustering is not used, there is no overhead except for a duplicate object with references to nodes and edges. | |||||
*/ | |||||
/** | |||||
* This function is only called by the setData function of the Network object. | |||||
* This loads the global references into the active sector. This initializes the sector. | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._putDataInSector = function() { | |||||
this.sectors["active"][this._sector()].nodes = this.nodes; | |||||
this.sectors["active"][this._sector()].edges = this.edges; | |||||
this.sectors["active"][this._sector()].nodeIndices = this.nodeIndices; | |||||
}; | |||||
/** | |||||
* /** | |||||
* This function sets the global references to nodes, edges and nodeIndices back to | |||||
* those of the supplied (active) sector. If a type is defined, do the specific type | |||||
* | |||||
* @param {String} sectorId | |||||
* @param {String} [sectorType] | "active" or "frozen" | |||||
* @private | |||||
*/ | |||||
exports._switchToSector = function(sectorId, sectorType) { | |||||
if (sectorType === undefined || sectorType == "active") { | |||||
this._switchToActiveSector(sectorId); | |||||
} | |||||
else { | |||||
this._switchToFrozenSector(sectorId); | |||||
} | |||||
}; | |||||
/** | |||||
* This function sets the global references to nodes, edges and nodeIndices back to | |||||
* those of the supplied active sector. | |||||
* | |||||
* @param sectorId | |||||
* @private | |||||
*/ | |||||
exports._switchToActiveSector = function(sectorId) { | |||||
this.nodeIndices = this.sectors["active"][sectorId]["nodeIndices"]; | |||||
this.nodes = this.sectors["active"][sectorId]["nodes"]; | |||||
this.edges = this.sectors["active"][sectorId]["edges"]; | |||||
}; | |||||
/** | |||||
* This function sets the global references to nodes, edges and nodeIndices back to | |||||
* those of the supplied active sector. | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._switchToSupportSector = function() { | |||||
this.nodeIndices = this.sectors["support"]["nodeIndices"]; | |||||
this.nodes = this.sectors["support"]["nodes"]; | |||||
this.edges = this.sectors["support"]["edges"]; | |||||
}; | |||||
/** | |||||
* This function sets the global references to nodes, edges and nodeIndices back to | |||||
* those of the supplied frozen sector. | |||||
* | |||||
* @param sectorId | |||||
* @private | |||||
*/ | |||||
exports._switchToFrozenSector = function(sectorId) { | |||||
this.nodeIndices = this.sectors["frozen"][sectorId]["nodeIndices"]; | |||||
this.nodes = this.sectors["frozen"][sectorId]["nodes"]; | |||||
this.edges = this.sectors["frozen"][sectorId]["edges"]; | |||||
}; | |||||
/** | |||||
* This function sets the global references to nodes, edges and nodeIndices back to | |||||
* those of the currently active sector. | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._loadLatestSector = function() { | |||||
this._switchToSector(this._sector()); | |||||
}; | |||||
/** | |||||
* This function returns the currently active sector Id | |||||
* | |||||
* @returns {String} | |||||
* @private | |||||
*/ | |||||
exports._sector = function() { | |||||
return this.activeSector[this.activeSector.length-1]; | |||||
}; | |||||
/** | |||||
* This function returns the previously active sector Id | |||||
* | |||||
* @returns {String} | |||||
* @private | |||||
*/ | |||||
exports._previousSector = function() { | |||||
if (this.activeSector.length > 1) { | |||||
return this.activeSector[this.activeSector.length-2]; | |||||
} | |||||
else { | |||||
throw new TypeError('there are not enough sectors in the this.activeSector array.'); | |||||
} | |||||
}; | |||||
/** | |||||
* We add the active sector at the end of the this.activeSector array | |||||
* This ensures it is the currently active sector returned by _sector() and it reaches the top | |||||
* of the activeSector stack. When we reverse our steps we move from the end to the beginning of this stack. | |||||
* | |||||
* @param newId | |||||
* @private | |||||
*/ | |||||
exports._setActiveSector = function(newId) { | |||||
this.activeSector.push(newId); | |||||
}; | |||||
/** | |||||
* We remove the currently active sector id from the active sector stack. This happens when | |||||
* we reactivate the previously active sector | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._forgetLastSector = function() { | |||||
this.activeSector.pop(); | |||||
}; | |||||
/** | |||||
* This function creates a new active sector with the supplied newId. This newId | |||||
* is the expanding node id. | |||||
* | |||||
* @param {String} newId | Id of the new active sector | |||||
* @private | |||||
*/ | |||||
exports._createNewSector = function(newId) { | |||||
// create the new sector | |||||
this.sectors["active"][newId] = {"nodes":{}, | |||||
"edges":{}, | |||||
"nodeIndices":[], | |||||
"formationScale": this.scale, | |||||
"drawingNode": undefined}; | |||||
// create the new sector render node. This gives visual feedback that you are in a new sector. | |||||
this.sectors["active"][newId]['drawingNode'] = new Node( | |||||
{id:newId, | |||||
color: { | |||||
background: "#eaefef", | |||||
border: "495c5e" | |||||
} | |||||
},{},{},this.constants); | |||||
this.sectors["active"][newId]['drawingNode'].clusterSize = 2; | |||||
}; | |||||
/** | |||||
* This function removes the currently active sector. This is called when we create a new | |||||
* active sector. | |||||
* | |||||
* @param {String} sectorId | Id of the active sector that will be removed | |||||
* @private | |||||
*/ | |||||
exports._deleteActiveSector = function(sectorId) { | |||||
delete this.sectors["active"][sectorId]; | |||||
}; | |||||
/** | |||||
* This function removes the currently active sector. This is called when we reactivate | |||||
* the previously active sector. | |||||
* | |||||
* @param {String} sectorId | Id of the active sector that will be removed | |||||
* @private | |||||
*/ | |||||
exports._deleteFrozenSector = function(sectorId) { | |||||
delete this.sectors["frozen"][sectorId]; | |||||
}; | |||||
/** | |||||
* Freezing an active sector means moving it from the "active" object to the "frozen" object. | |||||
* We copy the references, then delete the active entree. | |||||
* | |||||
* @param sectorId | |||||
* @private | |||||
*/ | |||||
exports._freezeSector = function(sectorId) { | |||||
// we move the set references from the active to the frozen stack. | |||||
this.sectors["frozen"][sectorId] = this.sectors["active"][sectorId]; | |||||
// we have moved the sector data into the frozen set, we now remove it from the active set | |||||
this._deleteActiveSector(sectorId); | |||||
}; | |||||
/** | |||||
* This is the reverse operation of _freezeSector. Activating means moving the sector from the "frozen" | |||||
* object to the "active" object. | |||||
* | |||||
* @param sectorId | |||||
* @private | |||||
*/ | |||||
exports._activateSector = function(sectorId) { | |||||
// we move the set references from the frozen to the active stack. | |||||
this.sectors["active"][sectorId] = this.sectors["frozen"][sectorId]; | |||||
// we have moved the sector data into the active set, we now remove it from the frozen stack | |||||
this._deleteFrozenSector(sectorId); | |||||
}; | |||||
/** | |||||
* This function merges the data from the currently active sector with a frozen sector. This is used | |||||
* in the process of reverting back to the previously active sector. | |||||
* The data that is placed in the frozen (the previously active) sector is the node that has been removed from it | |||||
* upon the creation of a new active sector. | |||||
* | |||||
* @param sectorId | |||||
* @private | |||||
*/ | |||||
exports._mergeThisWithFrozen = function(sectorId) { | |||||
// copy all nodes | |||||
for (var nodeId in this.nodes) { | |||||
if (this.nodes.hasOwnProperty(nodeId)) { | |||||
this.sectors["frozen"][sectorId]["nodes"][nodeId] = this.nodes[nodeId]; | |||||
} | |||||
} | |||||
// copy all edges (if not fully clustered, else there are no edges) | |||||
for (var edgeId in this.edges) { | |||||
if (this.edges.hasOwnProperty(edgeId)) { | |||||
this.sectors["frozen"][sectorId]["edges"][edgeId] = this.edges[edgeId]; | |||||
} | |||||
} | |||||
// merge the nodeIndices | |||||
for (var i = 0; i < this.nodeIndices.length; i++) { | |||||
this.sectors["frozen"][sectorId]["nodeIndices"].push(this.nodeIndices[i]); | |||||
} | |||||
}; | |||||
/** | |||||
* This clusters the sector to one cluster. It was a single cluster before this process started so | |||||
* we revert to that state. The clusterToFit function with a maximum size of 1 node does this. | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._collapseThisToSingleCluster = function() { | |||||
this.clusterToFit(1,false); | |||||
}; | |||||
/** | |||||
* We create a new active sector from the node that we want to open. | |||||
* | |||||
* @param node | |||||
* @private | |||||
*/ | |||||
exports._addSector = function(node) { | |||||
// this is the currently active sector | |||||
var sector = this._sector(); | |||||
// // this should allow me to select nodes from a frozen set. | |||||
// if (this.sectors['active'][sector]["nodes"].hasOwnProperty(node.id)) { | |||||
// console.log("the node is part of the active sector"); | |||||
// } | |||||
// else { | |||||
// console.log("I dont know what the fuck happened!!"); | |||||
// } | |||||
// when we switch to a new sector, we remove the node that will be expanded from the current nodes list. | |||||
delete this.nodes[node.id]; | |||||
var unqiueIdentifier = util.randomUUID(); | |||||
// we fully freeze the currently active sector | |||||
this._freezeSector(sector); | |||||
// we create a new active sector. This sector has the Id of the node to ensure uniqueness | |||||
this._createNewSector(unqiueIdentifier); | |||||
// we add the active sector to the sectors array to be able to revert these steps later on | |||||
this._setActiveSector(unqiueIdentifier); | |||||
// we redirect the global references to the new sector's references. this._sector() now returns unqiueIdentifier | |||||
this._switchToSector(this._sector()); | |||||
// finally we add the node we removed from our previous active sector to the new active sector | |||||
this.nodes[node.id] = node; | |||||
}; | |||||
/** | |||||
* We close the sector that is currently open and revert back to the one before. | |||||
* If the active sector is the "default" sector, nothing happens. | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._collapseSector = function() { | |||||
// the currently active sector | |||||
var sector = this._sector(); | |||||
// we cannot collapse the default sector | |||||
if (sector != "default") { | |||||
if ((this.nodeIndices.length == 1) || | |||||
(this.sectors["active"][sector]["drawingNode"].width*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) || | |||||
(this.sectors["active"][sector]["drawingNode"].height*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) { | |||||
var previousSector = this._previousSector(); | |||||
// we collapse the sector back to a single cluster | |||||
this._collapseThisToSingleCluster(); | |||||
// we move the remaining nodes, edges and nodeIndices to the previous sector. | |||||
// This previous sector is the one we will reactivate | |||||
this._mergeThisWithFrozen(previousSector); | |||||
// the previously active (frozen) sector now has all the data from the currently active sector. | |||||
// we can now delete the active sector. | |||||
this._deleteActiveSector(sector); | |||||
// we activate the previously active (and currently frozen) sector. | |||||
this._activateSector(previousSector); | |||||
// we load the references from the newly active sector into the global references | |||||
this._switchToSector(previousSector); | |||||
// we forget the previously active sector because we reverted to the one before | |||||
this._forgetLastSector(); | |||||
// finally, we update the node index list. | |||||
this._updateNodeIndexList(); | |||||
// we refresh the list with calulation nodes and calculation node indices. | |||||
this._updateCalculationNodes(); | |||||
} | |||||
} | |||||
}; | |||||
/** | |||||
* This runs a function in all active sectors. This is used in _redraw() and the _initializeForceCalculation(). | |||||
* | |||||
* @param {String} runFunction | This is the NAME of a function we want to call in all active sectors | |||||
* | we dont pass the function itself because then the "this" is the window object | |||||
* | instead of the Network object | |||||
* @param {*} [argument] | Optional: arguments to pass to the runFunction | |||||
* @private | |||||
*/ | |||||
exports._doInAllActiveSectors = function(runFunction,argument) { | |||||
if (argument === undefined) { | |||||
for (var sector in this.sectors["active"]) { | |||||
if (this.sectors["active"].hasOwnProperty(sector)) { | |||||
// switch the global references to those of this sector | |||||
this._switchToActiveSector(sector); | |||||
this[runFunction](); | |||||
} | |||||
} | |||||
} | |||||
else { | |||||
for (var sector in this.sectors["active"]) { | |||||
if (this.sectors["active"].hasOwnProperty(sector)) { | |||||
// switch the global references to those of this sector | |||||
this._switchToActiveSector(sector); | |||||
var args = Array.prototype.splice.call(arguments, 1); | |||||
if (args.length > 1) { | |||||
this[runFunction](args[0],args[1]); | |||||
} | |||||
else { | |||||
this[runFunction](argument); | |||||
} | |||||
} | |||||
} | |||||
} | |||||
// we revert the global references back to our active sector | |||||
this._loadLatestSector(); | |||||
}; | |||||
/** | |||||
* This runs a function in all active sectors. This is used in _redraw() and the _initializeForceCalculation(). | |||||
* | |||||
* @param {String} runFunction | This is the NAME of a function we want to call in all active sectors | |||||
* | we dont pass the function itself because then the "this" is the window object | |||||
* | instead of the Network object | |||||
* @param {*} [argument] | Optional: arguments to pass to the runFunction | |||||
* @private | |||||
*/ | |||||
exports._doInSupportSector = function(runFunction,argument) { | |||||
if (argument === undefined) { | |||||
this._switchToSupportSector(); | |||||
this[runFunction](); | |||||
} | |||||
else { | |||||
this._switchToSupportSector(); | |||||
var args = Array.prototype.splice.call(arguments, 1); | |||||
if (args.length > 1) { | |||||
this[runFunction](args[0],args[1]); | |||||
} | |||||
else { | |||||
this[runFunction](argument); | |||||
} | |||||
} | |||||
// we revert the global references back to our active sector | |||||
this._loadLatestSector(); | |||||
}; | |||||
/** | |||||
* This runs a function in all frozen sectors. This is used in the _redraw(). | |||||
* | |||||
* @param {String} runFunction | This is the NAME of a function we want to call in all active sectors | |||||
* | we don't pass the function itself because then the "this" is the window object | |||||
* | instead of the Network object | |||||
* @param {*} [argument] | Optional: arguments to pass to the runFunction | |||||
* @private | |||||
*/ | |||||
exports._doInAllFrozenSectors = function(runFunction,argument) { | |||||
if (argument === undefined) { | |||||
for (var sector in this.sectors["frozen"]) { | |||||
if (this.sectors["frozen"].hasOwnProperty(sector)) { | |||||
// switch the global references to those of this sector | |||||
this._switchToFrozenSector(sector); | |||||
this[runFunction](); | |||||
} | |||||
} | |||||
} | |||||
else { | |||||
for (var sector in this.sectors["frozen"]) { | |||||
if (this.sectors["frozen"].hasOwnProperty(sector)) { | |||||
// switch the global references to those of this sector | |||||
this._switchToFrozenSector(sector); | |||||
var args = Array.prototype.splice.call(arguments, 1); | |||||
if (args.length > 1) { | |||||
this[runFunction](args[0],args[1]); | |||||
} | |||||
else { | |||||
this[runFunction](argument); | |||||
} | |||||
} | |||||
} | |||||
} | |||||
this._loadLatestSector(); | |||||
}; | |||||
/** | |||||
* This runs a function in all sectors. This is used in the _redraw(). | |||||
* | |||||
* @param {String} runFunction | This is the NAME of a function we want to call in all active sectors | |||||
* | we don't pass the function itself because then the "this" is the window object | |||||
* | instead of the Network object | |||||
* @param {*} [argument] | Optional: arguments to pass to the runFunction | |||||
* @private | |||||
*/ | |||||
exports._doInAllSectors = function(runFunction,argument) { | |||||
var args = Array.prototype.splice.call(arguments, 1); | |||||
if (argument === undefined) { | |||||
this._doInAllActiveSectors(runFunction); | |||||
this._doInAllFrozenSectors(runFunction); | |||||
} | |||||
else { | |||||
if (args.length > 1) { | |||||
this._doInAllActiveSectors(runFunction,args[0],args[1]); | |||||
this._doInAllFrozenSectors(runFunction,args[0],args[1]); | |||||
} | |||||
else { | |||||
this._doInAllActiveSectors(runFunction,argument); | |||||
this._doInAllFrozenSectors(runFunction,argument); | |||||
} | |||||
} | |||||
}; | |||||
/** | |||||
* This clears the nodeIndices list. We cannot use this.nodeIndices = [] because we would break the link with the | |||||
* active sector. Thus we clear the nodeIndices in the active sector, then reconnect the this.nodeIndices to it. | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._clearNodeIndexList = function() { | |||||
var sector = this._sector(); | |||||
this.sectors["active"][sector]["nodeIndices"] = []; | |||||
this.nodeIndices = this.sectors["active"][sector]["nodeIndices"]; | |||||
}; | |||||
/** | |||||
* Draw the encompassing sector node | |||||
* | |||||
* @param ctx | |||||
* @param sectorType | |||||
* @private | |||||
*/ | |||||
exports._drawSectorNodes = function(ctx,sectorType) { | |||||
var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node; | |||||
for (var sector in this.sectors[sectorType]) { | |||||
if (this.sectors[sectorType].hasOwnProperty(sector)) { | |||||
if (this.sectors[sectorType][sector]["drawingNode"] !== undefined) { | |||||
this._switchToSector(sector,sectorType); | |||||
minY = 1e9; maxY = -1e9; minX = 1e9; maxX = -1e9; | |||||
for (var nodeId in this.nodes) { | |||||
if (this.nodes.hasOwnProperty(nodeId)) { | |||||
node = this.nodes[nodeId]; | |||||
node.resize(ctx); | |||||
if (minX > node.x - 0.5 * node.width) {minX = node.x - 0.5 * node.width;} | |||||
if (maxX < node.x + 0.5 * node.width) {maxX = node.x + 0.5 * node.width;} | |||||
if (minY > node.y - 0.5 * node.height) {minY = node.y - 0.5 * node.height;} | |||||
if (maxY < node.y + 0.5 * node.height) {maxY = node.y + 0.5 * node.height;} | |||||
} | |||||
} | |||||
node = this.sectors[sectorType][sector]["drawingNode"]; | |||||
node.x = 0.5 * (maxX + minX); | |||||
node.y = 0.5 * (maxY + minY); | |||||
node.width = 2 * (node.x - minX); | |||||
node.height = 2 * (node.y - minY); | |||||
node.radius = Math.sqrt(Math.pow(0.5*node.width,2) + Math.pow(0.5*node.height,2)); | |||||
node.setScale(this.scale); | |||||
node._drawCircle(ctx); | |||||
} | |||||
} | |||||
} | |||||
}; | |||||
exports._drawAllSectorNodes = function(ctx) { | |||||
this._drawSectorNodes(ctx,"frozen"); | |||||
this._drawSectorNodes(ctx,"active"); | |||||
this._loadLatestSector(); | |||||
}; |
@ -0,0 +1,705 @@ | |||||
var Node = require('../Node'); | |||||
/** | |||||
* This function can be called from the _doInAllSectors function | |||||
* | |||||
* @param object | |||||
* @param overlappingNodes | |||||
* @private | |||||
*/ | |||||
exports._getNodesOverlappingWith = function(object, overlappingNodes) { | |||||
var nodes = this.nodes; | |||||
for (var nodeId in nodes) { | |||||
if (nodes.hasOwnProperty(nodeId)) { | |||||
if (nodes[nodeId].isOverlappingWith(object)) { | |||||
overlappingNodes.push(nodeId); | |||||
} | |||||
} | |||||
} | |||||
}; | |||||
/** | |||||
* retrieve all nodes overlapping with given object | |||||
* @param {Object} object An object with parameters left, top, right, bottom | |||||
* @return {Number[]} An array with id's of the overlapping nodes | |||||
* @private | |||||
*/ | |||||
exports._getAllNodesOverlappingWith = function (object) { | |||||
var overlappingNodes = []; | |||||
this._doInAllActiveSectors("_getNodesOverlappingWith",object,overlappingNodes); | |||||
return overlappingNodes; | |||||
}; | |||||
/** | |||||
* Return a position object in canvasspace from a single point in screenspace | |||||
* | |||||
* @param pointer | |||||
* @returns {{left: number, top: number, right: number, bottom: number}} | |||||
* @private | |||||
*/ | |||||
exports._pointerToPositionObject = function(pointer) { | |||||
var x = this._XconvertDOMtoCanvas(pointer.x); | |||||
var y = this._YconvertDOMtoCanvas(pointer.y); | |||||
return { | |||||
left: x, | |||||
top: y, | |||||
right: x, | |||||
bottom: y | |||||
}; | |||||
}; | |||||
/** | |||||
* Get the top node at the a specific point (like a click) | |||||
* | |||||
* @param {{x: Number, y: Number}} pointer | |||||
* @return {Node | null} node | |||||
* @private | |||||
*/ | |||||
exports._getNodeAt = function (pointer) { | |||||
// we first check if this is an navigation controls element | |||||
var positionObject = this._pointerToPositionObject(pointer); | |||||
var overlappingNodes = this._getAllNodesOverlappingWith(positionObject); | |||||
// if there are overlapping nodes, select the last one, this is the | |||||
// one which is drawn on top of the others | |||||
if (overlappingNodes.length > 0) { | |||||
return this.nodes[overlappingNodes[overlappingNodes.length - 1]]; | |||||
} | |||||
else { | |||||
return null; | |||||
} | |||||
}; | |||||
/** | |||||
* retrieve all edges overlapping with given object, selector is around center | |||||
* @param {Object} object An object with parameters left, top, right, bottom | |||||
* @return {Number[]} An array with id's of the overlapping nodes | |||||
* @private | |||||
*/ | |||||
exports._getEdgesOverlappingWith = function (object, overlappingEdges) { | |||||
var edges = this.edges; | |||||
for (var edgeId in edges) { | |||||
if (edges.hasOwnProperty(edgeId)) { | |||||
if (edges[edgeId].isOverlappingWith(object)) { | |||||
overlappingEdges.push(edgeId); | |||||
} | |||||
} | |||||
} | |||||
}; | |||||
/** | |||||
* retrieve all nodes overlapping with given object | |||||
* @param {Object} object An object with parameters left, top, right, bottom | |||||
* @return {Number[]} An array with id's of the overlapping nodes | |||||
* @private | |||||
*/ | |||||
exports._getAllEdgesOverlappingWith = function (object) { | |||||
var overlappingEdges = []; | |||||
this._doInAllActiveSectors("_getEdgesOverlappingWith",object,overlappingEdges); | |||||
return overlappingEdges; | |||||
}; | |||||
/** | |||||
* Place holder. To implement change the _getNodeAt to a _getObjectAt. Have the _getObjectAt call | |||||
* _getNodeAt and _getEdgesAt, then priortize the selection to user preferences. | |||||
* | |||||
* @param pointer | |||||
* @returns {null} | |||||
* @private | |||||
*/ | |||||
exports._getEdgeAt = function(pointer) { | |||||
var positionObject = this._pointerToPositionObject(pointer); | |||||
var overlappingEdges = this._getAllEdgesOverlappingWith(positionObject); | |||||
if (overlappingEdges.length > 0) { | |||||
return this.edges[overlappingEdges[overlappingEdges.length - 1]]; | |||||
} | |||||
else { | |||||
return null; | |||||
} | |||||
}; | |||||
/** | |||||
* Add object to the selection array. | |||||
* | |||||
* @param obj | |||||
* @private | |||||
*/ | |||||
exports._addToSelection = function(obj) { | |||||
if (obj instanceof Node) { | |||||
this.selectionObj.nodes[obj.id] = obj; | |||||
} | |||||
else { | |||||
this.selectionObj.edges[obj.id] = obj; | |||||
} | |||||
}; | |||||
/** | |||||
* Add object to the selection array. | |||||
* | |||||
* @param obj | |||||
* @private | |||||
*/ | |||||
exports._addToHover = function(obj) { | |||||
if (obj instanceof Node) { | |||||
this.hoverObj.nodes[obj.id] = obj; | |||||
} | |||||
else { | |||||
this.hoverObj.edges[obj.id] = obj; | |||||
} | |||||
}; | |||||
/** | |||||
* Remove a single option from selection. | |||||
* | |||||
* @param {Object} obj | |||||
* @private | |||||
*/ | |||||
exports._removeFromSelection = function(obj) { | |||||
if (obj instanceof Node) { | |||||
delete this.selectionObj.nodes[obj.id]; | |||||
} | |||||
else { | |||||
delete this.selectionObj.edges[obj.id]; | |||||
} | |||||
}; | |||||
/** | |||||
* Unselect all. The selectionObj is useful for this. | |||||
* | |||||
* @param {Boolean} [doNotTrigger] | ignore trigger | |||||
* @private | |||||
*/ | |||||
exports._unselectAll = function(doNotTrigger) { | |||||
if (doNotTrigger === undefined) { | |||||
doNotTrigger = false; | |||||
} | |||||
for(var nodeId in this.selectionObj.nodes) { | |||||
if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { | |||||
this.selectionObj.nodes[nodeId].unselect(); | |||||
} | |||||
} | |||||
for(var edgeId in this.selectionObj.edges) { | |||||
if(this.selectionObj.edges.hasOwnProperty(edgeId)) { | |||||
this.selectionObj.edges[edgeId].unselect(); | |||||
} | |||||
} | |||||
this.selectionObj = {nodes:{},edges:{}}; | |||||
if (doNotTrigger == false) { | |||||
this.emit('select', this.getSelection()); | |||||
} | |||||
}; | |||||
/** | |||||
* Unselect all clusters. The selectionObj is useful for this. | |||||
* | |||||
* @param {Boolean} [doNotTrigger] | ignore trigger | |||||
* @private | |||||
*/ | |||||
exports._unselectClusters = function(doNotTrigger) { | |||||
if (doNotTrigger === undefined) { | |||||
doNotTrigger = false; | |||||
} | |||||
for (var nodeId in this.selectionObj.nodes) { | |||||
if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { | |||||
if (this.selectionObj.nodes[nodeId].clusterSize > 1) { | |||||
this.selectionObj.nodes[nodeId].unselect(); | |||||
this._removeFromSelection(this.selectionObj.nodes[nodeId]); | |||||
} | |||||
} | |||||
} | |||||
if (doNotTrigger == false) { | |||||
this.emit('select', this.getSelection()); | |||||
} | |||||
}; | |||||
/** | |||||
* return the number of selected nodes | |||||
* | |||||
* @returns {number} | |||||
* @private | |||||
*/ | |||||
exports._getSelectedNodeCount = function() { | |||||
var count = 0; | |||||
for (var nodeId in this.selectionObj.nodes) { | |||||
if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { | |||||
count += 1; | |||||
} | |||||
} | |||||
return count; | |||||
}; | |||||
/** | |||||
* return the selected node | |||||
* | |||||
* @returns {number} | |||||
* @private | |||||
*/ | |||||
exports._getSelectedNode = function() { | |||||
for (var nodeId in this.selectionObj.nodes) { | |||||
if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { | |||||
return this.selectionObj.nodes[nodeId]; | |||||
} | |||||
} | |||||
return null; | |||||
}; | |||||
/** | |||||
* return the selected edge | |||||
* | |||||
* @returns {number} | |||||
* @private | |||||
*/ | |||||
exports._getSelectedEdge = function() { | |||||
for (var edgeId in this.selectionObj.edges) { | |||||
if (this.selectionObj.edges.hasOwnProperty(edgeId)) { | |||||
return this.selectionObj.edges[edgeId]; | |||||
} | |||||
} | |||||
return null; | |||||
}; | |||||
/** | |||||
* return the number of selected edges | |||||
* | |||||
* @returns {number} | |||||
* @private | |||||
*/ | |||||
exports._getSelectedEdgeCount = function() { | |||||
var count = 0; | |||||
for (var edgeId in this.selectionObj.edges) { | |||||
if (this.selectionObj.edges.hasOwnProperty(edgeId)) { | |||||
count += 1; | |||||
} | |||||
} | |||||
return count; | |||||
}; | |||||
/** | |||||
* return the number of selected objects. | |||||
* | |||||
* @returns {number} | |||||
* @private | |||||
*/ | |||||
exports._getSelectedObjectCount = function() { | |||||
var count = 0; | |||||
for(var nodeId in this.selectionObj.nodes) { | |||||
if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { | |||||
count += 1; | |||||
} | |||||
} | |||||
for(var edgeId in this.selectionObj.edges) { | |||||
if(this.selectionObj.edges.hasOwnProperty(edgeId)) { | |||||
count += 1; | |||||
} | |||||
} | |||||
return count; | |||||
}; | |||||
/** | |||||
* Check if anything is selected | |||||
* | |||||
* @returns {boolean} | |||||
* @private | |||||
*/ | |||||
exports._selectionIsEmpty = function() { | |||||
for(var nodeId in this.selectionObj.nodes) { | |||||
if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { | |||||
return false; | |||||
} | |||||
} | |||||
for(var edgeId in this.selectionObj.edges) { | |||||
if(this.selectionObj.edges.hasOwnProperty(edgeId)) { | |||||
return false; | |||||
} | |||||
} | |||||
return true; | |||||
}; | |||||
/** | |||||
* check if one of the selected nodes is a cluster. | |||||
* | |||||
* @returns {boolean} | |||||
* @private | |||||
*/ | |||||
exports._clusterInSelection = function() { | |||||
for(var nodeId in this.selectionObj.nodes) { | |||||
if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { | |||||
if (this.selectionObj.nodes[nodeId].clusterSize > 1) { | |||||
return true; | |||||
} | |||||
} | |||||
} | |||||
return false; | |||||
}; | |||||
/** | |||||
* select the edges connected to the node that is being selected | |||||
* | |||||
* @param {Node} node | |||||
* @private | |||||
*/ | |||||
exports._selectConnectedEdges = function(node) { | |||||
for (var i = 0; i < node.dynamicEdges.length; i++) { | |||||
var edge = node.dynamicEdges[i]; | |||||
edge.select(); | |||||
this._addToSelection(edge); | |||||
} | |||||
}; | |||||
/** | |||||
* select the edges connected to the node that is being selected | |||||
* | |||||
* @param {Node} node | |||||
* @private | |||||
*/ | |||||
exports._hoverConnectedEdges = function(node) { | |||||
for (var i = 0; i < node.dynamicEdges.length; i++) { | |||||
var edge = node.dynamicEdges[i]; | |||||
edge.hover = true; | |||||
this._addToHover(edge); | |||||
} | |||||
}; | |||||
/** | |||||
* unselect the edges connected to the node that is being selected | |||||
* | |||||
* @param {Node} node | |||||
* @private | |||||
*/ | |||||
exports._unselectConnectedEdges = function(node) { | |||||
for (var i = 0; i < node.dynamicEdges.length; i++) { | |||||
var edge = node.dynamicEdges[i]; | |||||
edge.unselect(); | |||||
this._removeFromSelection(edge); | |||||
} | |||||
}; | |||||
/** | |||||
* This is called when someone clicks on a node. either select or deselect it. | |||||
* If there is an existing selection and we don't want to append to it, clear the existing selection | |||||
* | |||||
* @param {Node || Edge} object | |||||
* @param {Boolean} append | |||||
* @param {Boolean} [doNotTrigger] | ignore trigger | |||||
* @private | |||||
*/ | |||||
exports._selectObject = function(object, append, doNotTrigger, highlightEdges) { | |||||
if (doNotTrigger === undefined) { | |||||
doNotTrigger = false; | |||||
} | |||||
if (highlightEdges === undefined) { | |||||
highlightEdges = true; | |||||
} | |||||
if (this._selectionIsEmpty() == false && append == false && this.forceAppendSelection == false) { | |||||
this._unselectAll(true); | |||||
} | |||||
if (object.selected == false) { | |||||
object.select(); | |||||
this._addToSelection(object); | |||||
if (object instanceof Node && this.blockConnectingEdgeSelection == false && highlightEdges == true) { | |||||
this._selectConnectedEdges(object); | |||||
} | |||||
} | |||||
else { | |||||
object.unselect(); | |||||
this._removeFromSelection(object); | |||||
} | |||||
if (doNotTrigger == false) { | |||||
this.emit('select', this.getSelection()); | |||||
} | |||||
}; | |||||
/** | |||||
* This is called when someone clicks on a node. either select or deselect it. | |||||
* If there is an existing selection and we don't want to append to it, clear the existing selection | |||||
* | |||||
* @param {Node || Edge} object | |||||
* @private | |||||
*/ | |||||
exports._blurObject = function(object) { | |||||
if (object.hover == true) { | |||||
object.hover = false; | |||||
this.emit("blurNode",{node:object.id}); | |||||
} | |||||
}; | |||||
/** | |||||
* This is called when someone clicks on a node. either select or deselect it. | |||||
* If there is an existing selection and we don't want to append to it, clear the existing selection | |||||
* | |||||
* @param {Node || Edge} object | |||||
* @private | |||||
*/ | |||||
exports._hoverObject = function(object) { | |||||
if (object.hover == false) { | |||||
object.hover = true; | |||||
this._addToHover(object); | |||||
if (object instanceof Node) { | |||||
this.emit("hoverNode",{node:object.id}); | |||||
} | |||||
} | |||||
if (object instanceof Node) { | |||||
this._hoverConnectedEdges(object); | |||||
} | |||||
}; | |||||
/** | |||||
* handles the selection part of the touch, only for navigation controls elements; | |||||
* Touch is triggered before tap, also before hold. Hold triggers after a while. | |||||
* This is the most responsive solution | |||||
* | |||||
* @param {Object} pointer | |||||
* @private | |||||
*/ | |||||
exports._handleTouch = function(pointer) { | |||||
}; | |||||
/** | |||||
* handles the selection part of the tap; | |||||
* | |||||
* @param {Object} pointer | |||||
* @private | |||||
*/ | |||||
exports._handleTap = function(pointer) { | |||||
var node = this._getNodeAt(pointer); | |||||
if (node != null) { | |||||
this._selectObject(node,false); | |||||
} | |||||
else { | |||||
var edge = this._getEdgeAt(pointer); | |||||
if (edge != null) { | |||||
this._selectObject(edge,false); | |||||
} | |||||
else { | |||||
this._unselectAll(); | |||||
} | |||||
} | |||||
this.emit("click", this.getSelection()); | |||||
this._redraw(); | |||||
}; | |||||
/** | |||||
* handles the selection part of the double tap and opens a cluster if needed | |||||
* | |||||
* @param {Object} pointer | |||||
* @private | |||||
*/ | |||||
exports._handleDoubleTap = function(pointer) { | |||||
var node = this._getNodeAt(pointer); | |||||
if (node != null && node !== undefined) { | |||||
// we reset the areaCenter here so the opening of the node will occur | |||||
this.areaCenter = {"x" : this._XconvertDOMtoCanvas(pointer.x), | |||||
"y" : this._YconvertDOMtoCanvas(pointer.y)}; | |||||
this.openCluster(node); | |||||
} | |||||
this.emit("doubleClick", this.getSelection()); | |||||
}; | |||||
/** | |||||
* Handle the onHold selection part | |||||
* | |||||
* @param pointer | |||||
* @private | |||||
*/ | |||||
exports._handleOnHold = function(pointer) { | |||||
var node = this._getNodeAt(pointer); | |||||
if (node != null) { | |||||
this._selectObject(node,true); | |||||
} | |||||
else { | |||||
var edge = this._getEdgeAt(pointer); | |||||
if (edge != null) { | |||||
this._selectObject(edge,true); | |||||
} | |||||
} | |||||
this._redraw(); | |||||
}; | |||||
/** | |||||
* handle the onRelease event. These functions are here for the navigation controls module. | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._handleOnRelease = function(pointer) { | |||||
}; | |||||
/** | |||||
* | |||||
* retrieve the currently selected objects | |||||
* @return {{nodes: Array.<String>, edges: Array.<String>}} selection | |||||
*/ | |||||
exports.getSelection = function() { | |||||
var nodeIds = this.getSelectedNodes(); | |||||
var edgeIds = this.getSelectedEdges(); | |||||
return {nodes:nodeIds, edges:edgeIds}; | |||||
}; | |||||
/** | |||||
* | |||||
* retrieve the currently selected nodes | |||||
* @return {String[]} selection An array with the ids of the | |||||
* selected nodes. | |||||
*/ | |||||
exports.getSelectedNodes = function() { | |||||
var idArray = []; | |||||
for(var nodeId in this.selectionObj.nodes) { | |||||
if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { | |||||
idArray.push(nodeId); | |||||
} | |||||
} | |||||
return idArray | |||||
}; | |||||
/** | |||||
* | |||||
* retrieve the currently selected edges | |||||
* @return {Array} selection An array with the ids of the | |||||
* selected nodes. | |||||
*/ | |||||
exports.getSelectedEdges = function() { | |||||
var idArray = []; | |||||
for(var edgeId in this.selectionObj.edges) { | |||||
if(this.selectionObj.edges.hasOwnProperty(edgeId)) { | |||||
idArray.push(edgeId); | |||||
} | |||||
} | |||||
return idArray; | |||||
}; | |||||
/** | |||||
* select zero or more nodes | |||||
* @param {Number[] | String[]} selection An array with the ids of the | |||||
* selected nodes. | |||||
*/ | |||||
exports.setSelection = function(selection) { | |||||
var i, iMax, id; | |||||
if (!selection || (selection.length == undefined)) | |||||
throw 'Selection must be an array with ids'; | |||||
// first unselect any selected node | |||||
this._unselectAll(true); | |||||
for (i = 0, iMax = selection.length; i < iMax; i++) { | |||||
id = selection[i]; | |||||
var node = this.nodes[id]; | |||||
if (!node) { | |||||
throw new RangeError('Node with id "' + id + '" not found'); | |||||
} | |||||
this._selectObject(node,true,true); | |||||
} | |||||
console.log("setSelection is deprecated. Please use selectNodes instead.") | |||||
this.redraw(); | |||||
}; | |||||
/** | |||||
* select zero or more nodes with the option to highlight edges | |||||
* @param {Number[] | String[]} selection An array with the ids of the | |||||
* selected nodes. | |||||
* @param {boolean} [highlightEdges] | |||||
*/ | |||||
exports.selectNodes = function(selection, highlightEdges) { | |||||
var i, iMax, id; | |||||
if (!selection || (selection.length == undefined)) | |||||
throw 'Selection must be an array with ids'; | |||||
// first unselect any selected node | |||||
this._unselectAll(true); | |||||
for (i = 0, iMax = selection.length; i < iMax; i++) { | |||||
id = selection[i]; | |||||
var node = this.nodes[id]; | |||||
if (!node) { | |||||
throw new RangeError('Node with id "' + id + '" not found'); | |||||
} | |||||
this._selectObject(node,true,true,highlightEdges); | |||||
} | |||||
this.redraw(); | |||||
}; | |||||
/** | |||||
* select zero or more edges | |||||
* @param {Number[] | String[]} selection An array with the ids of the | |||||
* selected nodes. | |||||
*/ | |||||
exports.selectEdges = function(selection) { | |||||
var i, iMax, id; | |||||
if (!selection || (selection.length == undefined)) | |||||
throw 'Selection must be an array with ids'; | |||||
// first unselect any selected node | |||||
this._unselectAll(true); | |||||
for (i = 0, iMax = selection.length; i < iMax; i++) { | |||||
id = selection[i]; | |||||
var edge = this.edges[id]; | |||||
if (!edge) { | |||||
throw new RangeError('Edge with id "' + id + '" not found'); | |||||
} | |||||
this._selectObject(edge,true,true,highlightEdges); | |||||
} | |||||
this.redraw(); | |||||
}; | |||||
/** | |||||
* Validate the selection: remove ids of nodes which no longer exist | |||||
* @private | |||||
*/ | |||||
exports._updateSelection = function () { | |||||
for(var nodeId in this.selectionObj.nodes) { | |||||
if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { | |||||
if (!this.nodes.hasOwnProperty(nodeId)) { | |||||
delete this.selectionObj.nodes[nodeId]; | |||||
} | |||||
} | |||||
} | |||||
for(var edgeId in this.selectionObj.edges) { | |||||
if(this.selectionObj.edges.hasOwnProperty(edgeId)) { | |||||
if (!this.edges.hasOwnProperty(edgeId)) { | |||||
delete this.selectionObj.edges[edgeId]; | |||||
} | |||||
} | |||||
} | |||||
}; |
@ -0,0 +1,393 @@ | |||||
/** | |||||
* This function calculates the forces the nodes apply on eachother based on a gravitational model. | |||||
* The Barnes Hut method is used to speed up this N-body simulation. | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._calculateNodeForces = function() { | |||||
if (this.constants.physics.barnesHut.gravitationalConstant != 0) { | |||||
var node; | |||||
var nodes = this.calculationNodes; | |||||
var nodeIndices = this.calculationNodeIndices; | |||||
var nodeCount = nodeIndices.length; | |||||
this._formBarnesHutTree(nodes,nodeIndices); | |||||
var barnesHutTree = this.barnesHutTree; | |||||
// place the nodes one by one recursively | |||||
for (var i = 0; i < nodeCount; i++) { | |||||
node = nodes[nodeIndices[i]]; | |||||
// starting with root is irrelevant, it never passes the BarnesHut condition | |||||
this._getForceContribution(barnesHutTree.root.children.NW,node); | |||||
this._getForceContribution(barnesHutTree.root.children.NE,node); | |||||
this._getForceContribution(barnesHutTree.root.children.SW,node); | |||||
this._getForceContribution(barnesHutTree.root.children.SE,node); | |||||
} | |||||
} | |||||
}; | |||||
/** | |||||
* This function traverses the barnesHutTree. It checks when it can approximate distant nodes with their center of mass. | |||||
* If a region contains a single node, we check if it is not itself, then we apply the force. | |||||
* | |||||
* @param parentBranch | |||||
* @param node | |||||
* @private | |||||
*/ | |||||
exports._getForceContribution = function(parentBranch,node) { | |||||
// we get no force contribution from an empty region | |||||
if (parentBranch.childrenCount > 0) { | |||||
var dx,dy,distance; | |||||
// get the distance from the center of mass to the node. | |||||
dx = parentBranch.centerOfMass.x - node.x; | |||||
dy = parentBranch.centerOfMass.y - node.y; | |||||
distance = Math.sqrt(dx * dx + dy * dy); | |||||
// BarnesHut condition | |||||
// original condition : s/d < theta = passed === d/s > 1/theta = passed | |||||
// calcSize = 1/s --> d * 1/s > 1/theta = passed | |||||
if (distance * parentBranch.calcSize > this.constants.physics.barnesHut.theta) { | |||||
// duplicate code to reduce function calls to speed up program | |||||
if (distance == 0) { | |||||
distance = 0.1*Math.random(); | |||||
dx = distance; | |||||
} | |||||
var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.mass / (distance * distance * distance); | |||||
var fx = dx * gravityForce; | |||||
var fy = dy * gravityForce; | |||||
node.fx += fx; | |||||
node.fy += fy; | |||||
} | |||||
else { | |||||
// Did not pass the condition, go into children if available | |||||
if (parentBranch.childrenCount == 4) { | |||||
this._getForceContribution(parentBranch.children.NW,node); | |||||
this._getForceContribution(parentBranch.children.NE,node); | |||||
this._getForceContribution(parentBranch.children.SW,node); | |||||
this._getForceContribution(parentBranch.children.SE,node); | |||||
} | |||||
else { // parentBranch must have only one node, if it was empty we wouldnt be here | |||||
if (parentBranch.children.data.id != node.id) { // if it is not self | |||||
// duplicate code to reduce function calls to speed up program | |||||
if (distance == 0) { | |||||
distance = 0.5*Math.random(); | |||||
dx = distance; | |||||
} | |||||
var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.mass / (distance * distance * distance); | |||||
var fx = dx * gravityForce; | |||||
var fy = dy * gravityForce; | |||||
node.fx += fx; | |||||
node.fy += fy; | |||||
} | |||||
} | |||||
} | |||||
} | |||||
}; | |||||
/** | |||||
* This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes. | |||||
* | |||||
* @param nodes | |||||
* @param nodeIndices | |||||
* @private | |||||
*/ | |||||
exports._formBarnesHutTree = function(nodes,nodeIndices) { | |||||
var node; | |||||
var nodeCount = nodeIndices.length; | |||||
var minX = Number.MAX_VALUE, | |||||
minY = Number.MAX_VALUE, | |||||
maxX =-Number.MAX_VALUE, | |||||
maxY =-Number.MAX_VALUE; | |||||
// get the range of the nodes | |||||
for (var i = 0; i < nodeCount; i++) { | |||||
var x = nodes[nodeIndices[i]].x; | |||||
var y = nodes[nodeIndices[i]].y; | |||||
if (x < minX) { minX = x; } | |||||
if (x > maxX) { maxX = x; } | |||||
if (y < minY) { minY = y; } | |||||
if (y > maxY) { maxY = y; } | |||||
} | |||||
// make the range a square | |||||
var sizeDiff = Math.abs(maxX - minX) - Math.abs(maxY - minY); // difference between X and Y | |||||
if (sizeDiff > 0) {minY -= 0.5 * sizeDiff; maxY += 0.5 * sizeDiff;} // xSize > ySize | |||||
else {minX += 0.5 * sizeDiff; maxX -= 0.5 * sizeDiff;} // xSize < ySize | |||||
var minimumTreeSize = 1e-5; | |||||
var rootSize = Math.max(minimumTreeSize,Math.abs(maxX - minX)); | |||||
var halfRootSize = 0.5 * rootSize; | |||||
var centerX = 0.5 * (minX + maxX), centerY = 0.5 * (minY + maxY); | |||||
// construct the barnesHutTree | |||||
var barnesHutTree = { | |||||
root:{ | |||||
centerOfMass: {x:0, y:0}, | |||||
mass:0, | |||||
range: { | |||||
minX: centerX-halfRootSize,maxX:centerX+halfRootSize, | |||||
minY: centerY-halfRootSize,maxY:centerY+halfRootSize | |||||
}, | |||||
size: rootSize, | |||||
calcSize: 1 / rootSize, | |||||
children: { data:null}, | |||||
maxWidth: 0, | |||||
level: 0, | |||||
childrenCount: 4 | |||||
} | |||||
}; | |||||
this._splitBranch(barnesHutTree.root); | |||||
// place the nodes one by one recursively | |||||
for (i = 0; i < nodeCount; i++) { | |||||
node = nodes[nodeIndices[i]]; | |||||
this._placeInTree(barnesHutTree.root,node); | |||||
} | |||||
// make global | |||||
this.barnesHutTree = barnesHutTree | |||||
}; | |||||
/** | |||||
* this updates the mass of a branch. this is increased by adding a node. | |||||
* | |||||
* @param parentBranch | |||||
* @param node | |||||
* @private | |||||
*/ | |||||
exports._updateBranchMass = function(parentBranch, node) { | |||||
var totalMass = parentBranch.mass + node.mass; | |||||
var totalMassInv = 1/totalMass; | |||||
parentBranch.centerOfMass.x = parentBranch.centerOfMass.x * parentBranch.mass + node.x * node.mass; | |||||
parentBranch.centerOfMass.x *= totalMassInv; | |||||
parentBranch.centerOfMass.y = parentBranch.centerOfMass.y * parentBranch.mass + node.y * node.mass; | |||||
parentBranch.centerOfMass.y *= totalMassInv; | |||||
parentBranch.mass = totalMass; | |||||
var biggestSize = Math.max(Math.max(node.height,node.radius),node.width); | |||||
parentBranch.maxWidth = (parentBranch.maxWidth < biggestSize) ? biggestSize : parentBranch.maxWidth; | |||||
}; | |||||
/** | |||||
* determine in which branch the node will be placed. | |||||
* | |||||
* @param parentBranch | |||||
* @param node | |||||
* @param skipMassUpdate | |||||
* @private | |||||
*/ | |||||
exports._placeInTree = function(parentBranch,node,skipMassUpdate) { | |||||
if (skipMassUpdate != true || skipMassUpdate === undefined) { | |||||
// update the mass of the branch. | |||||
this._updateBranchMass(parentBranch,node); | |||||
} | |||||
if (parentBranch.children.NW.range.maxX > node.x) { // in NW or SW | |||||
if (parentBranch.children.NW.range.maxY > node.y) { // in NW | |||||
this._placeInRegion(parentBranch,node,"NW"); | |||||
} | |||||
else { // in SW | |||||
this._placeInRegion(parentBranch,node,"SW"); | |||||
} | |||||
} | |||||
else { // in NE or SE | |||||
if (parentBranch.children.NW.range.maxY > node.y) { // in NE | |||||
this._placeInRegion(parentBranch,node,"NE"); | |||||
} | |||||
else { // in SE | |||||
this._placeInRegion(parentBranch,node,"SE"); | |||||
} | |||||
} | |||||
}; | |||||
/** | |||||
* actually place the node in a region (or branch) | |||||
* | |||||
* @param parentBranch | |||||
* @param node | |||||
* @param region | |||||
* @private | |||||
*/ | |||||
exports._placeInRegion = function(parentBranch,node,region) { | |||||
switch (parentBranch.children[region].childrenCount) { | |||||
case 0: // place node here | |||||
parentBranch.children[region].children.data = node; | |||||
parentBranch.children[region].childrenCount = 1; | |||||
this._updateBranchMass(parentBranch.children[region],node); | |||||
break; | |||||
case 1: // convert into children | |||||
// if there are two nodes exactly overlapping (on init, on opening of cluster etc.) | |||||
// we move one node a pixel and we do not put it in the tree. | |||||
if (parentBranch.children[region].children.data.x == node.x && | |||||
parentBranch.children[region].children.data.y == node.y) { | |||||
node.x += Math.random(); | |||||
node.y += Math.random(); | |||||
} | |||||
else { | |||||
this._splitBranch(parentBranch.children[region]); | |||||
this._placeInTree(parentBranch.children[region],node); | |||||
} | |||||
break; | |||||
case 4: // place in branch | |||||
this._placeInTree(parentBranch.children[region],node); | |||||
break; | |||||
} | |||||
}; | |||||
/** | |||||
* this function splits a branch into 4 sub branches. If the branch contained a node, we place it in the subbranch | |||||
* after the split is complete. | |||||
* | |||||
* @param parentBranch | |||||
* @private | |||||
*/ | |||||
exports._splitBranch = function(parentBranch) { | |||||
// if the branch is shaded with a node, replace the node in the new subset. | |||||
var containedNode = null; | |||||
if (parentBranch.childrenCount == 1) { | |||||
containedNode = parentBranch.children.data; | |||||
parentBranch.mass = 0; parentBranch.centerOfMass.x = 0; parentBranch.centerOfMass.y = 0; | |||||
} | |||||
parentBranch.childrenCount = 4; | |||||
parentBranch.children.data = null; | |||||
this._insertRegion(parentBranch,"NW"); | |||||
this._insertRegion(parentBranch,"NE"); | |||||
this._insertRegion(parentBranch,"SW"); | |||||
this._insertRegion(parentBranch,"SE"); | |||||
if (containedNode != null) { | |||||
this._placeInTree(parentBranch,containedNode); | |||||
} | |||||
}; | |||||
/** | |||||
* This function subdivides the region into four new segments. | |||||
* Specifically, this inserts a single new segment. | |||||
* It fills the children section of the parentBranch | |||||
* | |||||
* @param parentBranch | |||||
* @param region | |||||
* @param parentRange | |||||
* @private | |||||
*/ | |||||
exports._insertRegion = function(parentBranch, region) { | |||||
var minX,maxX,minY,maxY; | |||||
var childSize = 0.5 * parentBranch.size; | |||||
switch (region) { | |||||
case "NW": | |||||
minX = parentBranch.range.minX; | |||||
maxX = parentBranch.range.minX + childSize; | |||||
minY = parentBranch.range.minY; | |||||
maxY = parentBranch.range.minY + childSize; | |||||
break; | |||||
case "NE": | |||||
minX = parentBranch.range.minX + childSize; | |||||
maxX = parentBranch.range.maxX; | |||||
minY = parentBranch.range.minY; | |||||
maxY = parentBranch.range.minY + childSize; | |||||
break; | |||||
case "SW": | |||||
minX = parentBranch.range.minX; | |||||
maxX = parentBranch.range.minX + childSize; | |||||
minY = parentBranch.range.minY + childSize; | |||||
maxY = parentBranch.range.maxY; | |||||
break; | |||||
case "SE": | |||||
minX = parentBranch.range.minX + childSize; | |||||
maxX = parentBranch.range.maxX; | |||||
minY = parentBranch.range.minY + childSize; | |||||
maxY = parentBranch.range.maxY; | |||||
break; | |||||
} | |||||
parentBranch.children[region] = { | |||||
centerOfMass:{x:0,y:0}, | |||||
mass:0, | |||||
range:{minX:minX,maxX:maxX,minY:minY,maxY:maxY}, | |||||
size: 0.5 * parentBranch.size, | |||||
calcSize: 2 * parentBranch.calcSize, | |||||
children: {data:null}, | |||||
maxWidth: 0, | |||||
level: parentBranch.level+1, | |||||
childrenCount: 0 | |||||
}; | |||||
}; | |||||
/** | |||||
* This function is for debugging purposed, it draws the tree. | |||||
* | |||||
* @param ctx | |||||
* @param color | |||||
* @private | |||||
*/ | |||||
exports._drawTree = function(ctx,color) { | |||||
if (this.barnesHutTree !== undefined) { | |||||
ctx.lineWidth = 1; | |||||
this._drawBranch(this.barnesHutTree.root,ctx,color); | |||||
} | |||||
}; | |||||
/** | |||||
* This function is for debugging purposes. It draws the branches recursively. | |||||
* | |||||
* @param branch | |||||
* @param ctx | |||||
* @param color | |||||
* @private | |||||
*/ | |||||
exports._drawBranch = function(branch,ctx,color) { | |||||
if (color === undefined) { | |||||
color = "#FF0000"; | |||||
} | |||||
if (branch.childrenCount == 4) { | |||||
this._drawBranch(branch.children.NW,ctx); | |||||
this._drawBranch(branch.children.NE,ctx); | |||||
this._drawBranch(branch.children.SE,ctx); | |||||
this._drawBranch(branch.children.SW,ctx); | |||||
} | |||||
ctx.strokeStyle = color; | |||||
ctx.beginPath(); | |||||
ctx.moveTo(branch.range.minX,branch.range.minY); | |||||
ctx.lineTo(branch.range.maxX,branch.range.minY); | |||||
ctx.stroke(); | |||||
ctx.beginPath(); | |||||
ctx.moveTo(branch.range.maxX,branch.range.minY); | |||||
ctx.lineTo(branch.range.maxX,branch.range.maxY); | |||||
ctx.stroke(); | |||||
ctx.beginPath(); | |||||
ctx.moveTo(branch.range.maxX,branch.range.maxY); | |||||
ctx.lineTo(branch.range.minX,branch.range.maxY); | |||||
ctx.stroke(); | |||||
ctx.beginPath(); | |||||
ctx.moveTo(branch.range.minX,branch.range.maxY); | |||||
ctx.lineTo(branch.range.minX,branch.range.minY); | |||||
ctx.stroke(); | |||||
/* | |||||
if (branch.mass > 0) { | |||||
ctx.circle(branch.centerOfMass.x, branch.centerOfMass.y, 3*branch.mass); | |||||
ctx.stroke(); | |||||
} | |||||
*/ | |||||
}; |
@ -0,0 +1,125 @@ | |||||
/** | |||||
* Calculate the forces the nodes apply on eachother based on a repulsion field. | |||||
* This field is linearly approximated. | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._calculateNodeForces = function () { | |||||
var dx, dy, distance, fx, fy, combinedClusterSize, | |||||
repulsingForce, node1, node2, i, j; | |||||
var nodes = this.calculationNodes; | |||||
var nodeIndices = this.calculationNodeIndices; | |||||
// approximation constants | |||||
var b = 5; | |||||
var a_base = 0.5 * -b; | |||||
// repulsing forces between nodes | |||||
var nodeDistance = this.constants.physics.hierarchicalRepulsion.nodeDistance; | |||||
var minimumDistance = nodeDistance; | |||||
var a = a_base / minimumDistance; | |||||
// we loop from i over all but the last entree in the array | |||||
// j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j | |||||
for (i = 0; i < nodeIndices.length - 1; i++) { | |||||
node1 = nodes[nodeIndices[i]]; | |||||
for (j = i + 1; j < nodeIndices.length; j++) { | |||||
node2 = nodes[nodeIndices[j]]; | |||||
if (node1.level == node2.level) { | |||||
dx = node2.x - node1.x; | |||||
dy = node2.y - node1.y; | |||||
distance = Math.sqrt(dx * dx + dy * dy); | |||||
if (distance < 2 * minimumDistance) { | |||||
repulsingForce = a * distance + b; | |||||
var c = 0.05; | |||||
var d = 2 * minimumDistance * 2 * c; | |||||
repulsingForce = c * Math.pow(distance,2) - d * distance + d*d/(4*c); | |||||
// normalize force with | |||||
if (distance == 0) { | |||||
distance = 0.01; | |||||
} | |||||
else { | |||||
repulsingForce = repulsingForce / distance; | |||||
} | |||||
fx = dx * repulsingForce; | |||||
fy = dy * repulsingForce; | |||||
node1.fx -= fx; | |||||
node1.fy -= fy; | |||||
node2.fx += fx; | |||||
node2.fy += fy; | |||||
} | |||||
} | |||||
} | |||||
} | |||||
}; | |||||
/** | |||||
* this function calculates the effects of the springs in the case of unsmooth curves. | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._calculateHierarchicalSpringForces = function () { | |||||
var edgeLength, edge, edgeId; | |||||
var dx, dy, fx, fy, springForce, distance; | |||||
var edges = this.edges; | |||||
// forces caused by the edges, modelled as springs | |||||
for (edgeId in edges) { | |||||
if (edges.hasOwnProperty(edgeId)) { | |||||
edge = edges[edgeId]; | |||||
if (edge.connected) { | |||||
// only calculate forces if nodes are in the same sector | |||||
if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) { | |||||
edgeLength = edge.customLength ? edge.length : this.constants.physics.springLength; | |||||
// this implies that the edges between big clusters are longer | |||||
edgeLength += (edge.to.clusterSize + edge.from.clusterSize - 2) * this.constants.clustering.edgeGrowth; | |||||
dx = (edge.from.x - edge.to.x); | |||||
dy = (edge.from.y - edge.to.y); | |||||
distance = Math.sqrt(dx * dx + dy * dy); | |||||
if (distance == 0) { | |||||
distance = 0.01; | |||||
} | |||||
distance = Math.max(0.8*edgeLength,Math.min(5*edgeLength, distance)); | |||||
// the 1/distance is so the fx and fy can be calculated without sine or cosine. | |||||
springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance; | |||||
fx = dx * springForce; | |||||
fy = dy * springForce; | |||||
edge.to.fx -= fx; | |||||
edge.to.fy -= fy; | |||||
edge.from.fx += fx; | |||||
edge.from.fy += fy; | |||||
var factor = 5; | |||||
if (distance > edgeLength) { | |||||
factor = 25; | |||||
} | |||||
if (edge.from.level > edge.to.level) { | |||||
edge.to.fx -= factor*fx; | |||||
edge.to.fy -= factor*fy; | |||||
} | |||||
else if (edge.from.level < edge.to.level) { | |||||
edge.from.fx += factor*fx; | |||||
edge.from.fy += factor*fy; | |||||
} | |||||
} | |||||
} | |||||
} | |||||
} | |||||
}; |
@ -0,0 +1,700 @@ | |||||
var util = require('../../../util'); | |||||
var RepulsionMixin = require('./RepulsionMixin'); | |||||
var HierarchialRepulsionMixin = require('./HierarchialRepulsionMixin'); | |||||
var BarnesHutMixin = require('./BarnesHutMixin'); | |||||
/** | |||||
* Toggling barnes Hut calculation on and off. | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._toggleBarnesHut = function () { | |||||
this.constants.physics.barnesHut.enabled = !this.constants.physics.barnesHut.enabled; | |||||
this._loadSelectedForceSolver(); | |||||
this.moving = true; | |||||
this.start(); | |||||
}; | |||||
/** | |||||
* This loads the node force solver based on the barnes hut or repulsion algorithm | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._loadSelectedForceSolver = function () { | |||||
// this overloads the this._calculateNodeForces | |||||
if (this.constants.physics.barnesHut.enabled == true) { | |||||
this._clearMixin(RepulsionMixin); | |||||
this._clearMixin(HierarchialRepulsionMixin); | |||||
this.constants.physics.centralGravity = this.constants.physics.barnesHut.centralGravity; | |||||
this.constants.physics.springLength = this.constants.physics.barnesHut.springLength; | |||||
this.constants.physics.springConstant = this.constants.physics.barnesHut.springConstant; | |||||
this.constants.physics.damping = this.constants.physics.barnesHut.damping; | |||||
this._loadMixin(BarnesHutMixin); | |||||
} | |||||
else if (this.constants.physics.hierarchicalRepulsion.enabled == true) { | |||||
this._clearMixin(BarnesHutMixin); | |||||
this._clearMixin(RepulsionMixin); | |||||
this.constants.physics.centralGravity = this.constants.physics.hierarchicalRepulsion.centralGravity; | |||||
this.constants.physics.springLength = this.constants.physics.hierarchicalRepulsion.springLength; | |||||
this.constants.physics.springConstant = this.constants.physics.hierarchicalRepulsion.springConstant; | |||||
this.constants.physics.damping = this.constants.physics.hierarchicalRepulsion.damping; | |||||
this._loadMixin(HierarchialRepulsionMixin); | |||||
} | |||||
else { | |||||
this._clearMixin(BarnesHutMixin); | |||||
this._clearMixin(HierarchialRepulsionMixin); | |||||
this.barnesHutTree = undefined; | |||||
this.constants.physics.centralGravity = this.constants.physics.repulsion.centralGravity; | |||||
this.constants.physics.springLength = this.constants.physics.repulsion.springLength; | |||||
this.constants.physics.springConstant = this.constants.physics.repulsion.springConstant; | |||||
this.constants.physics.damping = this.constants.physics.repulsion.damping; | |||||
this._loadMixin(RepulsionMixin); | |||||
} | |||||
}; | |||||
/** | |||||
* Before calculating the forces, we check if we need to cluster to keep up performance and we check | |||||
* if there is more than one node. If it is just one node, we dont calculate anything. | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._initializeForceCalculation = function () { | |||||
// stop calculation if there is only one node | |||||
if (this.nodeIndices.length == 1) { | |||||
this.nodes[this.nodeIndices[0]]._setForce(0, 0); | |||||
} | |||||
else { | |||||
// if there are too many nodes on screen, we cluster without repositioning | |||||
if (this.nodeIndices.length > this.constants.clustering.clusterThreshold && this.constants.clustering.enabled == true) { | |||||
this.clusterToFit(this.constants.clustering.reduceToNodes, false); | |||||
} | |||||
// we now start the force calculation | |||||
this._calculateForces(); | |||||
} | |||||
}; | |||||
/** | |||||
* Calculate the external forces acting on the nodes | |||||
* Forces are caused by: edges, repulsing forces between nodes, gravity | |||||
* @private | |||||
*/ | |||||
exports._calculateForces = function () { | |||||
// Gravity is required to keep separated groups from floating off | |||||
// the forces are reset to zero in this loop by using _setForce instead | |||||
// of _addForce | |||||
this._calculateGravitationalForces(); | |||||
this._calculateNodeForces(); | |||||
if (this.constants.smoothCurves == true) { | |||||
this._calculateSpringForcesWithSupport(); | |||||
} | |||||
else { | |||||
if (this.constants.physics.hierarchicalRepulsion.enabled == true) { | |||||
this._calculateHierarchicalSpringForces(); | |||||
} | |||||
else { | |||||
this._calculateSpringForces(); | |||||
} | |||||
} | |||||
}; | |||||
/** | |||||
* Smooth curves are created by adding invisible nodes in the center of the edges. These nodes are also | |||||
* handled in the calculateForces function. We then use a quadratic curve with the center node as control. | |||||
* This function joins the datanodes and invisible (called support) nodes into one object. | |||||
* We do this so we do not contaminate this.nodes with the support nodes. | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._updateCalculationNodes = function () { | |||||
if (this.constants.smoothCurves == true) { | |||||
this.calculationNodes = {}; | |||||
this.calculationNodeIndices = []; | |||||
for (var nodeId in this.nodes) { | |||||
if (this.nodes.hasOwnProperty(nodeId)) { | |||||
this.calculationNodes[nodeId] = this.nodes[nodeId]; | |||||
} | |||||
} | |||||
var supportNodes = this.sectors['support']['nodes']; | |||||
for (var supportNodeId in supportNodes) { | |||||
if (supportNodes.hasOwnProperty(supportNodeId)) { | |||||
if (this.edges.hasOwnProperty(supportNodes[supportNodeId].parentEdgeId)) { | |||||
this.calculationNodes[supportNodeId] = supportNodes[supportNodeId]; | |||||
} | |||||
else { | |||||
supportNodes[supportNodeId]._setForce(0, 0); | |||||
} | |||||
} | |||||
} | |||||
for (var idx in this.calculationNodes) { | |||||
if (this.calculationNodes.hasOwnProperty(idx)) { | |||||
this.calculationNodeIndices.push(idx); | |||||
} | |||||
} | |||||
} | |||||
else { | |||||
this.calculationNodes = this.nodes; | |||||
this.calculationNodeIndices = this.nodeIndices; | |||||
} | |||||
}; | |||||
/** | |||||
* this function applies the central gravity effect to keep groups from floating off | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._calculateGravitationalForces = function () { | |||||
var dx, dy, distance, node, i; | |||||
var nodes = this.calculationNodes; | |||||
var gravity = this.constants.physics.centralGravity; | |||||
var gravityForce = 0; | |||||
for (i = 0; i < this.calculationNodeIndices.length; i++) { | |||||
node = nodes[this.calculationNodeIndices[i]]; | |||||
node.damping = this.constants.physics.damping; // possibly add function to alter damping properties of clusters. | |||||
// gravity does not apply when we are in a pocket sector | |||||
if (this._sector() == "default" && gravity != 0) { | |||||
dx = -node.x; | |||||
dy = -node.y; | |||||
distance = Math.sqrt(dx * dx + dy * dy); | |||||
gravityForce = (distance == 0) ? 0 : (gravity / distance); | |||||
node.fx = dx * gravityForce; | |||||
node.fy = dy * gravityForce; | |||||
} | |||||
else { | |||||
node.fx = 0; | |||||
node.fy = 0; | |||||
} | |||||
} | |||||
}; | |||||
/** | |||||
* this function calculates the effects of the springs in the case of unsmooth curves. | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._calculateSpringForces = function () { | |||||
var edgeLength, edge, edgeId; | |||||
var dx, dy, fx, fy, springForce, distance; | |||||
var edges = this.edges; | |||||
// forces caused by the edges, modelled as springs | |||||
for (edgeId in edges) { | |||||
if (edges.hasOwnProperty(edgeId)) { | |||||
edge = edges[edgeId]; | |||||
if (edge.connected) { | |||||
// only calculate forces if nodes are in the same sector | |||||
if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) { | |||||
edgeLength = edge.customLength ? edge.length : this.constants.physics.springLength; | |||||
// this implies that the edges between big clusters are longer | |||||
edgeLength += (edge.to.clusterSize + edge.from.clusterSize - 2) * this.constants.clustering.edgeGrowth; | |||||
dx = (edge.from.x - edge.to.x); | |||||
dy = (edge.from.y - edge.to.y); | |||||
distance = Math.sqrt(dx * dx + dy * dy); | |||||
if (distance == 0) { | |||||
distance = 0.01; | |||||
} | |||||
// the 1/distance is so the fx and fy can be calculated without sine or cosine. | |||||
springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance; | |||||
fx = dx * springForce; | |||||
fy = dy * springForce; | |||||
edge.from.fx += fx; | |||||
edge.from.fy += fy; | |||||
edge.to.fx -= fx; | |||||
edge.to.fy -= fy; | |||||
} | |||||
} | |||||
} | |||||
} | |||||
}; | |||||
/** | |||||
* This function calculates the springforces on the nodes, accounting for the support nodes. | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._calculateSpringForcesWithSupport = function () { | |||||
var edgeLength, edge, edgeId, combinedClusterSize; | |||||
var edges = this.edges; | |||||
// forces caused by the edges, modelled as springs | |||||
for (edgeId in edges) { | |||||
if (edges.hasOwnProperty(edgeId)) { | |||||
edge = edges[edgeId]; | |||||
if (edge.connected) { | |||||
// only calculate forces if nodes are in the same sector | |||||
if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) { | |||||
if (edge.via != null) { | |||||
var node1 = edge.to; | |||||
var node2 = edge.via; | |||||
var node3 = edge.from; | |||||
edgeLength = edge.customLength ? edge.length : this.constants.physics.springLength; | |||||
combinedClusterSize = node1.clusterSize + node3.clusterSize - 2; | |||||
// this implies that the edges between big clusters are longer | |||||
edgeLength += combinedClusterSize * this.constants.clustering.edgeGrowth; | |||||
this._calculateSpringForce(node1, node2, 0.5 * edgeLength); | |||||
this._calculateSpringForce(node2, node3, 0.5 * edgeLength); | |||||
} | |||||
} | |||||
} | |||||
} | |||||
} | |||||
}; | |||||
/** | |||||
* This is the code actually performing the calculation for the function above. It is split out to avoid repetition. | |||||
* | |||||
* @param node1 | |||||
* @param node2 | |||||
* @param edgeLength | |||||
* @private | |||||
*/ | |||||
exports._calculateSpringForce = function (node1, node2, edgeLength) { | |||||
var dx, dy, fx, fy, springForce, distance; | |||||
dx = (node1.x - node2.x); | |||||
dy = (node1.y - node2.y); | |||||
distance = Math.sqrt(dx * dx + dy * dy); | |||||
if (distance == 0) { | |||||
distance = 0.01; | |||||
} | |||||
// the 1/distance is so the fx and fy can be calculated without sine or cosine. | |||||
springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance; | |||||
fx = dx * springForce; | |||||
fy = dy * springForce; | |||||
node1.fx += fx; | |||||
node1.fy += fy; | |||||
node2.fx -= fx; | |||||
node2.fy -= fy; | |||||
}; | |||||
/** | |||||
* Load the HTML for the physics config and bind it | |||||
* @private | |||||
*/ | |||||
exports._loadPhysicsConfiguration = function () { | |||||
if (this.physicsConfiguration === undefined) { | |||||
this.backupConstants = {}; | |||||
util.deepExtend(this.backupConstants,this.constants); | |||||
var hierarchicalLayoutDirections = ["LR", "RL", "UD", "DU"]; | |||||
this.physicsConfiguration = document.createElement('div'); | |||||
this.physicsConfiguration.className = "PhysicsConfiguration"; | |||||
this.physicsConfiguration.innerHTML = '' + | |||||
'<table><tr><td><b>Simulation Mode:</b></td></tr>' + | |||||
'<tr>' + | |||||
'<td width="120px"><input type="radio" name="graph_physicsMethod" id="graph_physicsMethod1" value="BH" checked="checked">Barnes Hut</td>' + | |||||
'<td width="120px"><input type="radio" name="graph_physicsMethod" id="graph_physicsMethod2" value="R">Repulsion</td>' + | |||||
'<td width="120px"><input type="radio" name="graph_physicsMethod" id="graph_physicsMethod3" value="H">Hierarchical</td>' + | |||||
'</tr>' + | |||||
'</table>' + | |||||
'<table id="graph_BH_table" style="display:none">' + | |||||
'<tr><td><b>Barnes Hut</b></td></tr>' + | |||||
'<tr>' + | |||||
'<td width="150px">gravitationalConstant</td><td>0</td><td><input type="range" min="0" max="20000" value="' + (-1 * this.constants.physics.barnesHut.gravitationalConstant) + '" step="25" style="width:300px" id="graph_BH_gc"></td><td width="50px">-20000</td><td><input value="' + (-1 * this.constants.physics.barnesHut.gravitationalConstant) + '" id="graph_BH_gc_value" style="width:60px"></td>' + | |||||
'</tr>' + | |||||
'<tr>' + | |||||
'<td width="150px">centralGravity</td><td>0</td><td><input type="range" min="0" max="3" value="' + this.constants.physics.barnesHut.centralGravity + '" step="0.05" style="width:300px" id="graph_BH_cg"></td><td>3</td><td><input value="' + this.constants.physics.barnesHut.centralGravity + '" id="graph_BH_cg_value" style="width:60px"></td>' + | |||||
'</tr>' + | |||||
'<tr>' + | |||||
'<td width="150px">springLength</td><td>0</td><td><input type="range" min="0" max="500" value="' + this.constants.physics.barnesHut.springLength + '" step="1" style="width:300px" id="graph_BH_sl"></td><td>500</td><td><input value="' + this.constants.physics.barnesHut.springLength + '" id="graph_BH_sl_value" style="width:60px"></td>' + | |||||
'</tr>' + | |||||
'<tr>' + | |||||
'<td width="150px">springConstant</td><td>0</td><td><input type="range" min="0" max="0.5" value="' + this.constants.physics.barnesHut.springConstant + '" step="0.001" style="width:300px" id="graph_BH_sc"></td><td>0.5</td><td><input value="' + this.constants.physics.barnesHut.springConstant + '" id="graph_BH_sc_value" style="width:60px"></td>' + | |||||
'</tr>' + | |||||
'<tr>' + | |||||
'<td width="150px">damping</td><td>0</td><td><input type="range" min="0" max="0.3" value="' + this.constants.physics.barnesHut.damping + '" step="0.005" style="width:300px" id="graph_BH_damp"></td><td>0.3</td><td><input value="' + this.constants.physics.barnesHut.damping + '" id="graph_BH_damp_value" style="width:60px"></td>' + | |||||
'</tr>' + | |||||
'</table>' + | |||||
'<table id="graph_R_table" style="display:none">' + | |||||
'<tr><td><b>Repulsion</b></td></tr>' + | |||||
'<tr>' + | |||||
'<td width="150px">nodeDistance</td><td>0</td><td><input type="range" min="0" max="300" value="' + this.constants.physics.repulsion.nodeDistance + '" step="1" style="width:300px" id="graph_R_nd"></td><td width="50px">300</td><td><input value="' + this.constants.physics.repulsion.nodeDistance + '" id="graph_R_nd_value" style="width:60px"></td>' + | |||||
'</tr>' + | |||||
'<tr>' + | |||||
'<td width="150px">centralGravity</td><td>0</td><td><input type="range" min="0" max="3" value="' + this.constants.physics.repulsion.centralGravity + '" step="0.05" style="width:300px" id="graph_R_cg"></td><td>3</td><td><input value="' + this.constants.physics.repulsion.centralGravity + '" id="graph_R_cg_value" style="width:60px"></td>' + | |||||
'</tr>' + | |||||
'<tr>' + | |||||
'<td width="150px">springLength</td><td>0</td><td><input type="range" min="0" max="500" value="' + this.constants.physics.repulsion.springLength + '" step="1" style="width:300px" id="graph_R_sl"></td><td>500</td><td><input value="' + this.constants.physics.repulsion.springLength + '" id="graph_R_sl_value" style="width:60px"></td>' + | |||||
'</tr>' + | |||||
'<tr>' + | |||||
'<td width="150px">springConstant</td><td>0</td><td><input type="range" min="0" max="0.5" value="' + this.constants.physics.repulsion.springConstant + '" step="0.001" style="width:300px" id="graph_R_sc"></td><td>0.5</td><td><input value="' + this.constants.physics.repulsion.springConstant + '" id="graph_R_sc_value" style="width:60px"></td>' + | |||||
'</tr>' + | |||||
'<tr>' + | |||||
'<td width="150px">damping</td><td>0</td><td><input type="range" min="0" max="0.3" value="' + this.constants.physics.repulsion.damping + '" step="0.005" style="width:300px" id="graph_R_damp"></td><td>0.3</td><td><input value="' + this.constants.physics.repulsion.damping + '" id="graph_R_damp_value" style="width:60px"></td>' + | |||||
'</tr>' + | |||||
'</table>' + | |||||
'<table id="graph_H_table" style="display:none">' + | |||||
'<tr><td width="150"><b>Hierarchical</b></td></tr>' + | |||||
'<tr>' + | |||||
'<td width="150px">nodeDistance</td><td>0</td><td><input type="range" min="0" max="300" value="' + this.constants.physics.hierarchicalRepulsion.nodeDistance + '" step="1" style="width:300px" id="graph_H_nd"></td><td width="50px">300</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.nodeDistance + '" id="graph_H_nd_value" style="width:60px"></td>' + | |||||
'</tr>' + | |||||
'<tr>' + | |||||
'<td width="150px">centralGravity</td><td>0</td><td><input type="range" min="0" max="3" value="' + this.constants.physics.hierarchicalRepulsion.centralGravity + '" step="0.05" style="width:300px" id="graph_H_cg"></td><td>3</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.centralGravity + '" id="graph_H_cg_value" style="width:60px"></td>' + | |||||
'</tr>' + | |||||
'<tr>' + | |||||
'<td width="150px">springLength</td><td>0</td><td><input type="range" min="0" max="500" value="' + this.constants.physics.hierarchicalRepulsion.springLength + '" step="1" style="width:300px" id="graph_H_sl"></td><td>500</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.springLength + '" id="graph_H_sl_value" style="width:60px"></td>' + | |||||
'</tr>' + | |||||
'<tr>' + | |||||
'<td width="150px">springConstant</td><td>0</td><td><input type="range" min="0" max="0.5" value="' + this.constants.physics.hierarchicalRepulsion.springConstant + '" step="0.001" style="width:300px" id="graph_H_sc"></td><td>0.5</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.springConstant + '" id="graph_H_sc_value" style="width:60px"></td>' + | |||||
'</tr>' + | |||||
'<tr>' + | |||||
'<td width="150px">damping</td><td>0</td><td><input type="range" min="0" max="0.3" value="' + this.constants.physics.hierarchicalRepulsion.damping + '" step="0.005" style="width:300px" id="graph_H_damp"></td><td>0.3</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.damping + '" id="graph_H_damp_value" style="width:60px"></td>' + | |||||
'</tr>' + | |||||
'<tr>' + | |||||
'<td width="150px">direction</td><td>1</td><td><input type="range" min="0" max="3" value="' + hierarchicalLayoutDirections.indexOf(this.constants.hierarchicalLayout.direction) + '" step="1" style="width:300px" id="graph_H_direction"></td><td>4</td><td><input value="' + this.constants.hierarchicalLayout.direction + '" id="graph_H_direction_value" style="width:60px"></td>' + | |||||
'</tr>' + | |||||
'<tr>' + | |||||
'<td width="150px">levelSeparation</td><td>1</td><td><input type="range" min="0" max="500" value="' + this.constants.hierarchicalLayout.levelSeparation + '" step="1" style="width:300px" id="graph_H_levsep"></td><td>500</td><td><input value="' + this.constants.hierarchicalLayout.levelSeparation + '" id="graph_H_levsep_value" style="width:60px"></td>' + | |||||
'</tr>' + | |||||
'<tr>' + | |||||
'<td width="150px">nodeSpacing</td><td>1</td><td><input type="range" min="0" max="500" value="' + this.constants.hierarchicalLayout.nodeSpacing + '" step="1" style="width:300px" id="graph_H_nspac"></td><td>500</td><td><input value="' + this.constants.hierarchicalLayout.nodeSpacing + '" id="graph_H_nspac_value" style="width:60px"></td>' + | |||||
'</tr>' + | |||||
'</table>' + | |||||
'<table><tr><td><b>Options:</b></td></tr>' + | |||||
'<tr>' + | |||||
'<td width="180px"><input type="button" id="graph_toggleSmooth" value="Toggle smoothCurves" style="width:150px"></td>' + | |||||
'<td width="180px"><input type="button" id="graph_repositionNodes" value="Reinitialize" style="width:150px"></td>' + | |||||
'<td width="180px"><input type="button" id="graph_generateOptions" value="Generate Options" style="width:150px"></td>' + | |||||
'</tr>' + | |||||
'</table>' | |||||
this.containerElement.parentElement.insertBefore(this.physicsConfiguration, this.containerElement); | |||||
this.optionsDiv = document.createElement("div"); | |||||
this.optionsDiv.style.fontSize = "14px"; | |||||
this.optionsDiv.style.fontFamily = "verdana"; | |||||
this.containerElement.parentElement.insertBefore(this.optionsDiv, this.containerElement); | |||||
var rangeElement; | |||||
rangeElement = document.getElementById('graph_BH_gc'); | |||||
rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_gc', -1, "physics_barnesHut_gravitationalConstant"); | |||||
rangeElement = document.getElementById('graph_BH_cg'); | |||||
rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_cg', 1, "physics_centralGravity"); | |||||
rangeElement = document.getElementById('graph_BH_sc'); | |||||
rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sc', 1, "physics_springConstant"); | |||||
rangeElement = document.getElementById('graph_BH_sl'); | |||||
rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sl', 1, "physics_springLength"); | |||||
rangeElement = document.getElementById('graph_BH_damp'); | |||||
rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_damp', 1, "physics_damping"); | |||||
rangeElement = document.getElementById('graph_R_nd'); | |||||
rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_nd', 1, "physics_repulsion_nodeDistance"); | |||||
rangeElement = document.getElementById('graph_R_cg'); | |||||
rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_cg', 1, "physics_centralGravity"); | |||||
rangeElement = document.getElementById('graph_R_sc'); | |||||
rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sc', 1, "physics_springConstant"); | |||||
rangeElement = document.getElementById('graph_R_sl'); | |||||
rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sl', 1, "physics_springLength"); | |||||
rangeElement = document.getElementById('graph_R_damp'); | |||||
rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_damp', 1, "physics_damping"); | |||||
rangeElement = document.getElementById('graph_H_nd'); | |||||
rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nd', 1, "physics_hierarchicalRepulsion_nodeDistance"); | |||||
rangeElement = document.getElementById('graph_H_cg'); | |||||
rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_cg', 1, "physics_centralGravity"); | |||||
rangeElement = document.getElementById('graph_H_sc'); | |||||
rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sc', 1, "physics_springConstant"); | |||||
rangeElement = document.getElementById('graph_H_sl'); | |||||
rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sl', 1, "physics_springLength"); | |||||
rangeElement = document.getElementById('graph_H_damp'); | |||||
rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_damp', 1, "physics_damping"); | |||||
rangeElement = document.getElementById('graph_H_direction'); | |||||
rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_direction', hierarchicalLayoutDirections, "hierarchicalLayout_direction"); | |||||
rangeElement = document.getElementById('graph_H_levsep'); | |||||
rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_levsep', 1, "hierarchicalLayout_levelSeparation"); | |||||
rangeElement = document.getElementById('graph_H_nspac'); | |||||
rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nspac', 1, "hierarchicalLayout_nodeSpacing"); | |||||
var radioButton1 = document.getElementById("graph_physicsMethod1"); | |||||
var radioButton2 = document.getElementById("graph_physicsMethod2"); | |||||
var radioButton3 = document.getElementById("graph_physicsMethod3"); | |||||
radioButton2.checked = true; | |||||
if (this.constants.physics.barnesHut.enabled) { | |||||
radioButton1.checked = true; | |||||
} | |||||
if (this.constants.hierarchicalLayout.enabled) { | |||||
radioButton3.checked = true; | |||||
} | |||||
var graph_toggleSmooth = document.getElementById("graph_toggleSmooth"); | |||||
var graph_repositionNodes = document.getElementById("graph_repositionNodes"); | |||||
var graph_generateOptions = document.getElementById("graph_generateOptions"); | |||||
graph_toggleSmooth.onclick = graphToggleSmoothCurves.bind(this); | |||||
graph_repositionNodes.onclick = graphRepositionNodes.bind(this); | |||||
graph_generateOptions.onclick = graphGenerateOptions.bind(this); | |||||
if (this.constants.smoothCurves == true) { | |||||
graph_toggleSmooth.style.background = "#A4FF56"; | |||||
} | |||||
else { | |||||
graph_toggleSmooth.style.background = "#FF8532"; | |||||
} | |||||
switchConfigurations.apply(this); | |||||
radioButton1.onchange = switchConfigurations.bind(this); | |||||
radioButton2.onchange = switchConfigurations.bind(this); | |||||
radioButton3.onchange = switchConfigurations.bind(this); | |||||
} | |||||
}; | |||||
/** | |||||
* This overwrites the this.constants. | |||||
* | |||||
* @param constantsVariableName | |||||
* @param value | |||||
* @private | |||||
*/ | |||||
exports._overWriteGraphConstants = function (constantsVariableName, value) { | |||||
var nameArray = constantsVariableName.split("_"); | |||||
if (nameArray.length == 1) { | |||||
this.constants[nameArray[0]] = value; | |||||
} | |||||
else if (nameArray.length == 2) { | |||||
this.constants[nameArray[0]][nameArray[1]] = value; | |||||
} | |||||
else if (nameArray.length == 3) { | |||||
this.constants[nameArray[0]][nameArray[1]][nameArray[2]] = value; | |||||
} | |||||
}; | |||||
/** | |||||
* this function is bound to the toggle smooth curves button. That is also why it is not in the prototype. | |||||
*/ | |||||
function graphToggleSmoothCurves () { | |||||
this.constants.smoothCurves = !this.constants.smoothCurves; | |||||
var graph_toggleSmooth = document.getElementById("graph_toggleSmooth"); | |||||
if (this.constants.smoothCurves == true) {graph_toggleSmooth.style.background = "#A4FF56";} | |||||
else {graph_toggleSmooth.style.background = "#FF8532";} | |||||
this._configureSmoothCurves(false); | |||||
} | |||||
/** | |||||
* this function is used to scramble the nodes | |||||
* | |||||
*/ | |||||
function graphRepositionNodes () { | |||||
for (var nodeId in this.calculationNodes) { | |||||
if (this.calculationNodes.hasOwnProperty(nodeId)) { | |||||
this.calculationNodes[nodeId].vx = 0; this.calculationNodes[nodeId].vy = 0; | |||||
this.calculationNodes[nodeId].fx = 0; this.calculationNodes[nodeId].fy = 0; | |||||
} | |||||
} | |||||
if (this.constants.hierarchicalLayout.enabled == true) { | |||||
this._setupHierarchicalLayout(); | |||||
} | |||||
else { | |||||
this.repositionNodes(); | |||||
} | |||||
this.moving = true; | |||||
this.start(); | |||||
} | |||||
/** | |||||
* this is used to generate an options file from the playing with physics system. | |||||
*/ | |||||
function graphGenerateOptions () { | |||||
var options = "No options are required, default values used."; | |||||
var optionsSpecific = []; | |||||
var radioButton1 = document.getElementById("graph_physicsMethod1"); | |||||
var radioButton2 = document.getElementById("graph_physicsMethod2"); | |||||
if (radioButton1.checked == true) { | |||||
if (this.constants.physics.barnesHut.gravitationalConstant != this.backupConstants.physics.barnesHut.gravitationalConstant) {optionsSpecific.push("gravitationalConstant: " + this.constants.physics.barnesHut.gravitationalConstant);} | |||||
if (this.constants.physics.centralGravity != this.backupConstants.physics.barnesHut.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);} | |||||
if (this.constants.physics.springLength != this.backupConstants.physics.barnesHut.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);} | |||||
if (this.constants.physics.springConstant != this.backupConstants.physics.barnesHut.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);} | |||||
if (this.constants.physics.damping != this.backupConstants.physics.barnesHut.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);} | |||||
if (optionsSpecific.length != 0) { | |||||
options = "var options = {"; | |||||
options += "physics: {barnesHut: {"; | |||||
for (var i = 0; i < optionsSpecific.length; i++) { | |||||
options += optionsSpecific[i]; | |||||
if (i < optionsSpecific.length - 1) { | |||||
options += ", " | |||||
} | |||||
} | |||||
options += '}}' | |||||
} | |||||
if (this.constants.smoothCurves != this.backupConstants.smoothCurves) { | |||||
if (optionsSpecific.length == 0) {options = "var options = {";} | |||||
else {options += ", "} | |||||
options += "smoothCurves: " + this.constants.smoothCurves; | |||||
} | |||||
if (options != "No options are required, default values used.") { | |||||
options += '};' | |||||
} | |||||
} | |||||
else if (radioButton2.checked == true) { | |||||
options = "var options = {"; | |||||
options += "physics: {barnesHut: {enabled: false}"; | |||||
if (this.constants.physics.repulsion.nodeDistance != this.backupConstants.physics.repulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.repulsion.nodeDistance);} | |||||
if (this.constants.physics.centralGravity != this.backupConstants.physics.repulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);} | |||||
if (this.constants.physics.springLength != this.backupConstants.physics.repulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);} | |||||
if (this.constants.physics.springConstant != this.backupConstants.physics.repulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);} | |||||
if (this.constants.physics.damping != this.backupConstants.physics.repulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);} | |||||
if (optionsSpecific.length != 0) { | |||||
options += ", repulsion: {"; | |||||
for (var i = 0; i < optionsSpecific.length; i++) { | |||||
options += optionsSpecific[i]; | |||||
if (i < optionsSpecific.length - 1) { | |||||
options += ", " | |||||
} | |||||
} | |||||
options += '}}' | |||||
} | |||||
if (optionsSpecific.length == 0) {options += "}"} | |||||
if (this.constants.smoothCurves != this.backupConstants.smoothCurves) { | |||||
options += ", smoothCurves: " + this.constants.smoothCurves; | |||||
} | |||||
options += '};' | |||||
} | |||||
else { | |||||
options = "var options = {"; | |||||
if (this.constants.physics.hierarchicalRepulsion.nodeDistance != this.backupConstants.physics.hierarchicalRepulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.hierarchicalRepulsion.nodeDistance);} | |||||
if (this.constants.physics.centralGravity != this.backupConstants.physics.hierarchicalRepulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);} | |||||
if (this.constants.physics.springLength != this.backupConstants.physics.hierarchicalRepulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);} | |||||
if (this.constants.physics.springConstant != this.backupConstants.physics.hierarchicalRepulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);} | |||||
if (this.constants.physics.damping != this.backupConstants.physics.hierarchicalRepulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);} | |||||
if (optionsSpecific.length != 0) { | |||||
options += "physics: {hierarchicalRepulsion: {"; | |||||
for (var i = 0; i < optionsSpecific.length; i++) { | |||||
options += optionsSpecific[i]; | |||||
if (i < optionsSpecific.length - 1) { | |||||
options += ", "; | |||||
} | |||||
} | |||||
options += '}},'; | |||||
} | |||||
options += 'hierarchicalLayout: {'; | |||||
optionsSpecific = []; | |||||
if (this.constants.hierarchicalLayout.direction != this.backupConstants.hierarchicalLayout.direction) {optionsSpecific.push("direction: " + this.constants.hierarchicalLayout.direction);} | |||||
if (Math.abs(this.constants.hierarchicalLayout.levelSeparation) != this.backupConstants.hierarchicalLayout.levelSeparation) {optionsSpecific.push("levelSeparation: " + this.constants.hierarchicalLayout.levelSeparation);} | |||||
if (this.constants.hierarchicalLayout.nodeSpacing != this.backupConstants.hierarchicalLayout.nodeSpacing) {optionsSpecific.push("nodeSpacing: " + this.constants.hierarchicalLayout.nodeSpacing);} | |||||
if (optionsSpecific.length != 0) { | |||||
for (var i = 0; i < optionsSpecific.length; i++) { | |||||
options += optionsSpecific[i]; | |||||
if (i < optionsSpecific.length - 1) { | |||||
options += ", " | |||||
} | |||||
} | |||||
options += '}' | |||||
} | |||||
else { | |||||
options += "enabled:true}"; | |||||
} | |||||
options += '};' | |||||
} | |||||
this.optionsDiv.innerHTML = options; | |||||
} | |||||
/** | |||||
* this is used to switch between barnesHut, repulsion and hierarchical. | |||||
* | |||||
*/ | |||||
function switchConfigurations () { | |||||
var ids = ["graph_BH_table", "graph_R_table", "graph_H_table"]; | |||||
var radioButton = document.querySelector('input[name="graph_physicsMethod"]:checked').value; | |||||
var tableId = "graph_" + radioButton + "_table"; | |||||
var table = document.getElementById(tableId); | |||||
table.style.display = "block"; | |||||
for (var i = 0; i < ids.length; i++) { | |||||
if (ids[i] != tableId) { | |||||
table = document.getElementById(ids[i]); | |||||
table.style.display = "none"; | |||||
} | |||||
} | |||||
this._restoreNodes(); | |||||
if (radioButton == "R") { | |||||
this.constants.hierarchicalLayout.enabled = false; | |||||
this.constants.physics.hierarchicalRepulsion.enabled = false; | |||||
this.constants.physics.barnesHut.enabled = false; | |||||
} | |||||
else if (radioButton == "H") { | |||||
if (this.constants.hierarchicalLayout.enabled == false) { | |||||
this.constants.hierarchicalLayout.enabled = true; | |||||
this.constants.physics.hierarchicalRepulsion.enabled = true; | |||||
this.constants.physics.barnesHut.enabled = false; | |||||
this._setupHierarchicalLayout(); | |||||
} | |||||
} | |||||
else { | |||||
this.constants.hierarchicalLayout.enabled = false; | |||||
this.constants.physics.hierarchicalRepulsion.enabled = false; | |||||
this.constants.physics.barnesHut.enabled = true; | |||||
} | |||||
this._loadSelectedForceSolver(); | |||||
var graph_toggleSmooth = document.getElementById("graph_toggleSmooth"); | |||||
if (this.constants.smoothCurves == true) {graph_toggleSmooth.style.background = "#A4FF56";} | |||||
else {graph_toggleSmooth.style.background = "#FF8532";} | |||||
this.moving = true; | |||||
this.start(); | |||||
} | |||||
/** | |||||
* this generates the ranges depending on the iniital values. | |||||
* | |||||
* @param id | |||||
* @param map | |||||
* @param constantsVariableName | |||||
*/ | |||||
function showValueOfRange (id,map,constantsVariableName) { | |||||
var valueId = id + "_value"; | |||||
var rangeValue = document.getElementById(id).value; | |||||
if (map instanceof Array) { | |||||
document.getElementById(valueId).value = map[parseInt(rangeValue)]; | |||||
this._overWriteGraphConstants(constantsVariableName,map[parseInt(rangeValue)]); | |||||
} | |||||
else { | |||||
document.getElementById(valueId).value = parseInt(map) * parseFloat(rangeValue); | |||||
this._overWriteGraphConstants(constantsVariableName, parseInt(map) * parseFloat(rangeValue)); | |||||
} | |||||
if (constantsVariableName == "hierarchicalLayout_direction" || | |||||
constantsVariableName == "hierarchicalLayout_levelSeparation" || | |||||
constantsVariableName == "hierarchicalLayout_nodeSpacing") { | |||||
this._setupHierarchicalLayout(); | |||||
} | |||||
this.moving = true; | |||||
this.start(); | |||||
} |
@ -0,0 +1,58 @@ | |||||
/** | |||||
* Calculate the forces the nodes apply on each other based on a repulsion field. | |||||
* This field is linearly approximated. | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._calculateNodeForces = function () { | |||||
var dx, dy, angle, distance, fx, fy, combinedClusterSize, | |||||
repulsingForce, node1, node2, i, j; | |||||
var nodes = this.calculationNodes; | |||||
var nodeIndices = this.calculationNodeIndices; | |||||
// approximation constants | |||||
var a_base = -2 / 3; | |||||
var b = 4 / 3; | |||||
// repulsing forces between nodes | |||||
var nodeDistance = this.constants.physics.repulsion.nodeDistance; | |||||
var minimumDistance = nodeDistance; | |||||
// we loop from i over all but the last entree in the array | |||||
// j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j | |||||
for (i = 0; i < nodeIndices.length - 1; i++) { | |||||
node1 = nodes[nodeIndices[i]]; | |||||
for (j = i + 1; j < nodeIndices.length; j++) { | |||||
node2 = nodes[nodeIndices[j]]; | |||||
combinedClusterSize = node1.clusterSize + node2.clusterSize - 2; | |||||
dx = node2.x - node1.x; | |||||
dy = node2.y - node1.y; | |||||
distance = Math.sqrt(dx * dx + dy * dy); | |||||
minimumDistance = (combinedClusterSize == 0) ? nodeDistance : (nodeDistance * (1 + combinedClusterSize * this.constants.clustering.distanceAmplification)); | |||||
var a = a_base / minimumDistance; | |||||
if (distance < 2 * minimumDistance) { | |||||
if (distance < 0.5 * minimumDistance) { | |||||
repulsingForce = 1.0; | |||||
} | |||||
else { | |||||
repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)) | |||||
} | |||||
// amplify the repulsion for clusters. | |||||
repulsingForce *= (combinedClusterSize == 0) ? 1 : 1 + combinedClusterSize * this.constants.clustering.forceAmplification; | |||||
repulsingForce = repulsingForce / distance; | |||||
fx = dx * repulsingForce; | |||||
fy = dy * repulsingForce; | |||||
node1.fx -= fx; | |||||
node1.fy -= fy; | |||||
node2.fx += fx; | |||||
node2.fy += fy; | |||||
} | |||||
} | |||||
} | |||||
}; |
@ -1,84 +0,0 @@ | |||||
/** | |||||
* vis.js module exports | |||||
*/ | |||||
var vis = { | |||||
moment: moment, | |||||
util: util, | |||||
DOMutil: DOMutil, | |||||
DataSet: DataSet, | |||||
DataView: DataView, | |||||
Timeline: Timeline, | |||||
Graph2d: Graph2d, | |||||
timeline: { | |||||
DataStep: DataStep, | |||||
Range: Range, | |||||
stack: stack, | |||||
TimeStep: TimeStep, | |||||
components: { | |||||
items: { | |||||
Item: Item, | |||||
ItemBox: ItemBox, | |||||
ItemPoint: ItemPoint, | |||||
ItemRange: ItemRange | |||||
}, | |||||
Component: Component, | |||||
CurrentTime: CurrentTime, | |||||
CustomTime: CustomTime, | |||||
DataAxis: DataAxis, | |||||
GraphGroup: GraphGroup, | |||||
Group: Group, | |||||
ItemSet: ItemSet, | |||||
Legend: Legend, | |||||
LineGraph: LineGraph, | |||||
TimeAxis: TimeAxis | |||||
} | |||||
}, | |||||
Network: Network, | |||||
network: { | |||||
Edge: Edge, | |||||
Groups: Groups, | |||||
Images: Images, | |||||
Node: Node, | |||||
Popup: Popup | |||||
}, | |||||
// Deprecated since v3.0.0 | |||||
Graph: function () { | |||||
throw new Error('Graph is renamed to Network. Please create a graph as new vis.Network(...)'); | |||||
}, | |||||
Graph3d: Graph3d | |||||
}; | |||||
/** | |||||
* CommonJS module exports | |||||
*/ | |||||
if (typeof exports !== 'undefined') { | |||||
exports = vis; | |||||
} | |||||
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { | |||||
module.exports = vis; | |||||
} | |||||
/** | |||||
* AMD module exports | |||||
*/ | |||||
if (typeof(define) === 'function') { | |||||
define(function () { | |||||
return vis; | |||||
}); | |||||
} | |||||
/** | |||||
* Window exports | |||||
*/ | |||||
if (typeof window !== 'undefined') { | |||||
// attach the module to the window, load as a regular javascript file | |||||
window['vis'] = vis; | |||||
} |
@ -1,31 +0,0 @@ | |||||
/** | |||||
* vis.js module imports | |||||
*/ | |||||
// Try to load dependencies from the global window object. | |||||
// If not available there, load via require. | |||||
var moment = (typeof window !== 'undefined') && window['moment'] || require('moment'); | |||||
var Emitter = require('emitter-component'); | |||||
var Hammer; | |||||
if (typeof window !== 'undefined') { | |||||
// load hammer.js only when running in a browser (where window is available) | |||||
Hammer = window['Hammer'] || require('hammerjs'); | |||||
} | |||||
else { | |||||
Hammer = function () { | |||||
throw Error('hammer.js is only available in a browser, not in node.js.'); | |||||
} | |||||
} | |||||
var mousetrap; | |||||
if (typeof window !== 'undefined') { | |||||
// load mousetrap.js only when running in a browser (where window is available) | |||||
mousetrap = window['mousetrap'] || require('mousetrap'); | |||||
} | |||||
else { | |||||
mousetrap = function () { | |||||
throw Error('mouseTrap is only available in a browser, not in node.js.'); | |||||
} | |||||
} |
@ -1,829 +0,0 @@ | |||||
(function(exports) { | |||||
/** | |||||
* Parse a text source containing data in DOT language into a JSON object. | |||||
* The object contains two lists: one with nodes and one with edges. | |||||
* | |||||
* DOT language reference: http://www.graphviz.org/doc/info/lang.html | |||||
* | |||||
* @param {String} data Text containing a graph in DOT-notation | |||||
* @return {Object} graph An object containing two parameters: | |||||
* {Object[]} nodes | |||||
* {Object[]} edges | |||||
*/ | |||||
function parseDOT (data) { | |||||
dot = data; | |||||
return parseGraph(); | |||||
} | |||||
// token types enumeration | |||||
var TOKENTYPE = { | |||||
NULL : 0, | |||||
DELIMITER : 1, | |||||
IDENTIFIER: 2, | |||||
UNKNOWN : 3 | |||||
}; | |||||
// map with all delimiters | |||||
var DELIMITERS = { | |||||
'{': true, | |||||
'}': true, | |||||
'[': true, | |||||
']': true, | |||||
';': true, | |||||
'=': true, | |||||
',': true, | |||||
'->': true, | |||||
'--': true | |||||
}; | |||||
var dot = ''; // current dot file | |||||
var index = 0; // current index in dot file | |||||
var c = ''; // current token character in expr | |||||
var token = ''; // current token | |||||
var tokenType = TOKENTYPE.NULL; // type of the token | |||||
/** | |||||
* Get the first character from the dot file. | |||||
* The character is stored into the char c. If the end of the dot file is | |||||
* reached, the function puts an empty string in c. | |||||
*/ | |||||
function first() { | |||||
index = 0; | |||||
c = dot.charAt(0); | |||||
} | |||||
/** | |||||
* Get the next character from the dot file. | |||||
* The character is stored into the char c. If the end of the dot file is | |||||
* reached, the function puts an empty string in c. | |||||
*/ | |||||
function next() { | |||||
index++; | |||||
c = dot.charAt(index); | |||||
} | |||||
/** | |||||
* Preview the next character from the dot file. | |||||
* @return {String} cNext | |||||
*/ | |||||
function nextPreview() { | |||||
return dot.charAt(index + 1); | |||||
} | |||||
/** | |||||
* Test whether given character is alphabetic or numeric | |||||
* @param {String} c | |||||
* @return {Boolean} isAlphaNumeric | |||||
*/ | |||||
var regexAlphaNumeric = /[a-zA-Z_0-9.:#]/; | |||||
function isAlphaNumeric(c) { | |||||
return regexAlphaNumeric.test(c); | |||||
} | |||||
/** | |||||
* Merge all properties of object b into object b | |||||
* @param {Object} a | |||||
* @param {Object} b | |||||
* @return {Object} a | |||||
*/ | |||||
function merge (a, b) { | |||||
if (!a) { | |||||
a = {}; | |||||
} | |||||
if (b) { | |||||
for (var name in b) { | |||||
if (b.hasOwnProperty(name)) { | |||||
a[name] = b[name]; | |||||
} | |||||
} | |||||
} | |||||
return a; | |||||
} | |||||
/** | |||||
* Set a value in an object, where the provided parameter name can be a | |||||
* path with nested parameters. For example: | |||||
* | |||||
* var obj = {a: 2}; | |||||
* setValue(obj, 'b.c', 3); // obj = {a: 2, b: {c: 3}} | |||||
* | |||||
* @param {Object} obj | |||||
* @param {String} path A parameter name or dot-separated parameter path, | |||||
* like "color.highlight.border". | |||||
* @param {*} value | |||||
*/ | |||||
function setValue(obj, path, value) { | |||||
var keys = path.split('.'); | |||||
var o = obj; | |||||
while (keys.length) { | |||||
var key = keys.shift(); | |||||
if (keys.length) { | |||||
// this isn't the end point | |||||
if (!o[key]) { | |||||
o[key] = {}; | |||||
} | |||||
o = o[key]; | |||||
} | |||||
else { | |||||
// this is the end point | |||||
o[key] = value; | |||||
} | |||||
} | |||||
} | |||||
/** | |||||
* Add a node to a graph object. If there is already a node with | |||||
* the same id, their attributes will be merged. | |||||
* @param {Object} graph | |||||
* @param {Object} node | |||||
*/ | |||||
function addNode(graph, node) { | |||||
var i, len; | |||||
var current = null; | |||||
// find root graph (in case of subgraph) | |||||
var graphs = [graph]; // list with all graphs from current graph to root graph | |||||
var root = graph; | |||||
while (root.parent) { | |||||
graphs.push(root.parent); | |||||
root = root.parent; | |||||
} | |||||
// find existing node (at root level) by its id | |||||
if (root.nodes) { | |||||
for (i = 0, len = root.nodes.length; i < len; i++) { | |||||
if (node.id === root.nodes[i].id) { | |||||
current = root.nodes[i]; | |||||
break; | |||||
} | |||||
} | |||||
} | |||||
if (!current) { | |||||
// this is a new node | |||||
current = { | |||||
id: node.id | |||||
}; | |||||
if (graph.node) { | |||||
// clone default attributes | |||||
current.attr = merge(current.attr, graph.node); | |||||
} | |||||
} | |||||
// add node to this (sub)graph and all its parent graphs | |||||
for (i = graphs.length - 1; i >= 0; i--) { | |||||
var g = graphs[i]; | |||||
if (!g.nodes) { | |||||
g.nodes = []; | |||||
} | |||||
if (g.nodes.indexOf(current) == -1) { | |||||
g.nodes.push(current); | |||||
} | |||||
} | |||||
// merge attributes | |||||
if (node.attr) { | |||||
current.attr = merge(current.attr, node.attr); | |||||
} | |||||
} | |||||
/** | |||||
* Add an edge to a graph object | |||||
* @param {Object} graph | |||||
* @param {Object} edge | |||||
*/ | |||||
function addEdge(graph, edge) { | |||||
if (!graph.edges) { | |||||
graph.edges = []; | |||||
} | |||||
graph.edges.push(edge); | |||||
if (graph.edge) { | |||||
var attr = merge({}, graph.edge); // clone default attributes | |||||
edge.attr = merge(attr, edge.attr); // merge attributes | |||||
} | |||||
} | |||||
/** | |||||
* Create an edge to a graph object | |||||
* @param {Object} graph | |||||
* @param {String | Number | Object} from | |||||
* @param {String | Number | Object} to | |||||
* @param {String} type | |||||
* @param {Object | null} attr | |||||
* @return {Object} edge | |||||
*/ | |||||
function createEdge(graph, from, to, type, attr) { | |||||
var edge = { | |||||
from: from, | |||||
to: to, | |||||
type: type | |||||
}; | |||||
if (graph.edge) { | |||||
edge.attr = merge({}, graph.edge); // clone default attributes | |||||
} | |||||
edge.attr = merge(edge.attr || {}, attr); // merge attributes | |||||
return edge; | |||||
} | |||||
/** | |||||
* Get next token in the current dot file. | |||||
* The token and token type are available as token and tokenType | |||||
*/ | |||||
function getToken() { | |||||
tokenType = TOKENTYPE.NULL; | |||||
token = ''; | |||||
// skip over whitespaces | |||||
while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter | |||||
next(); | |||||
} | |||||
do { | |||||
var isComment = false; | |||||
// skip comment | |||||
if (c == '#') { | |||||
// find the previous non-space character | |||||
var i = index - 1; | |||||
while (dot.charAt(i) == ' ' || dot.charAt(i) == '\t') { | |||||
i--; | |||||
} | |||||
if (dot.charAt(i) == '\n' || dot.charAt(i) == '') { | |||||
// the # is at the start of a line, this is indeed a line comment | |||||
while (c != '' && c != '\n') { | |||||
next(); | |||||
} | |||||
isComment = true; | |||||
} | |||||
} | |||||
if (c == '/' && nextPreview() == '/') { | |||||
// skip line comment | |||||
while (c != '' && c != '\n') { | |||||
next(); | |||||
} | |||||
isComment = true; | |||||
} | |||||
if (c == '/' && nextPreview() == '*') { | |||||
// skip block comment | |||||
while (c != '') { | |||||
if (c == '*' && nextPreview() == '/') { | |||||
// end of block comment found. skip these last two characters | |||||
next(); | |||||
next(); | |||||
break; | |||||
} | |||||
else { | |||||
next(); | |||||
} | |||||
} | |||||
isComment = true; | |||||
} | |||||
// skip over whitespaces | |||||
while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter | |||||
next(); | |||||
} | |||||
} | |||||
while (isComment); | |||||
// check for end of dot file | |||||
if (c == '') { | |||||
// token is still empty | |||||
tokenType = TOKENTYPE.DELIMITER; | |||||
return; | |||||
} | |||||
// check for delimiters consisting of 2 characters | |||||
var c2 = c + nextPreview(); | |||||
if (DELIMITERS[c2]) { | |||||
tokenType = TOKENTYPE.DELIMITER; | |||||
token = c2; | |||||
next(); | |||||
next(); | |||||
return; | |||||
} | |||||
// check for delimiters consisting of 1 character | |||||
if (DELIMITERS[c]) { | |||||
tokenType = TOKENTYPE.DELIMITER; | |||||
token = c; | |||||
next(); | |||||
return; | |||||
} | |||||
// check for an identifier (number or string) | |||||
// TODO: more precise parsing of numbers/strings (and the port separator ':') | |||||
if (isAlphaNumeric(c) || c == '-') { | |||||
token += c; | |||||
next(); | |||||
while (isAlphaNumeric(c)) { | |||||
token += c; | |||||
next(); | |||||
} | |||||
if (token == 'false') { | |||||
token = false; // convert to boolean | |||||
} | |||||
else if (token == 'true') { | |||||
token = true; // convert to boolean | |||||
} | |||||
else if (!isNaN(Number(token))) { | |||||
token = Number(token); // convert to number | |||||
} | |||||
tokenType = TOKENTYPE.IDENTIFIER; | |||||
return; | |||||
} | |||||
// check for a string enclosed by double quotes | |||||
if (c == '"') { | |||||
next(); | |||||
while (c != '' && (c != '"' || (c == '"' && nextPreview() == '"'))) { | |||||
token += c; | |||||
if (c == '"') { // skip the escape character | |||||
next(); | |||||
} | |||||
next(); | |||||
} | |||||
if (c != '"') { | |||||
throw newSyntaxError('End of string " expected'); | |||||
} | |||||
next(); | |||||
tokenType = TOKENTYPE.IDENTIFIER; | |||||
return; | |||||
} | |||||
// something unknown is found, wrong characters, a syntax error | |||||
tokenType = TOKENTYPE.UNKNOWN; | |||||
while (c != '') { | |||||
token += c; | |||||
next(); | |||||
} | |||||
throw new SyntaxError('Syntax error in part "' + chop(token, 30) + '"'); | |||||
} | |||||
/** | |||||
* Parse a graph. | |||||
* @returns {Object} graph | |||||
*/ | |||||
function parseGraph() { | |||||
var graph = {}; | |||||
first(); | |||||
getToken(); | |||||
// optional strict keyword | |||||
if (token == 'strict') { | |||||
graph.strict = true; | |||||
getToken(); | |||||
} | |||||
// graph or digraph keyword | |||||
if (token == 'graph' || token == 'digraph') { | |||||
graph.type = token; | |||||
getToken(); | |||||
} | |||||
// optional graph id | |||||
if (tokenType == TOKENTYPE.IDENTIFIER) { | |||||
graph.id = token; | |||||
getToken(); | |||||
} | |||||
// open angle bracket | |||||
if (token != '{') { | |||||
throw newSyntaxError('Angle bracket { expected'); | |||||
} | |||||
getToken(); | |||||
// statements | |||||
parseStatements(graph); | |||||
// close angle bracket | |||||
if (token != '}') { | |||||
throw newSyntaxError('Angle bracket } expected'); | |||||
} | |||||
getToken(); | |||||
// end of file | |||||
if (token !== '') { | |||||
throw newSyntaxError('End of file expected'); | |||||
} | |||||
getToken(); | |||||
// remove temporary default properties | |||||
delete graph.node; | |||||
delete graph.edge; | |||||
delete graph.graph; | |||||
return graph; | |||||
} | |||||
/** | |||||
* Parse a list with statements. | |||||
* @param {Object} graph | |||||
*/ | |||||
function parseStatements (graph) { | |||||
while (token !== '' && token != '}') { | |||||
parseStatement(graph); | |||||
if (token == ';') { | |||||
getToken(); | |||||
} | |||||
} | |||||
} | |||||
/** | |||||
* Parse a single statement. Can be a an attribute statement, node | |||||
* statement, a series of node statements and edge statements, or a | |||||
* parameter. | |||||
* @param {Object} graph | |||||
*/ | |||||
function parseStatement(graph) { | |||||
// parse subgraph | |||||
var subgraph = parseSubgraph(graph); | |||||
if (subgraph) { | |||||
// edge statements | |||||
parseEdge(graph, subgraph); | |||||
return; | |||||
} | |||||
// parse an attribute statement | |||||
var attr = parseAttributeStatement(graph); | |||||
if (attr) { | |||||
return; | |||||
} | |||||
// parse node | |||||
if (tokenType != TOKENTYPE.IDENTIFIER) { | |||||
throw newSyntaxError('Identifier expected'); | |||||
} | |||||
var id = token; // id can be a string or a number | |||||
getToken(); | |||||
if (token == '=') { | |||||
// id statement | |||||
getToken(); | |||||
if (tokenType != TOKENTYPE.IDENTIFIER) { | |||||
throw newSyntaxError('Identifier expected'); | |||||
} | |||||
graph[id] = token; | |||||
getToken(); | |||||
// TODO: implement comma separated list with "a_list: ID=ID [','] [a_list] " | |||||
} | |||||
else { | |||||
parseNodeStatement(graph, id); | |||||
} | |||||
} | |||||
/** | |||||
* Parse a subgraph | |||||
* @param {Object} graph parent graph object | |||||
* @return {Object | null} subgraph | |||||
*/ | |||||
function parseSubgraph (graph) { | |||||
var subgraph = null; | |||||
// optional subgraph keyword | |||||
if (token == 'subgraph') { | |||||
subgraph = {}; | |||||
subgraph.type = 'subgraph'; | |||||
getToken(); | |||||
// optional graph id | |||||
if (tokenType == TOKENTYPE.IDENTIFIER) { | |||||
subgraph.id = token; | |||||
getToken(); | |||||
} | |||||
} | |||||
// open angle bracket | |||||
if (token == '{') { | |||||
getToken(); | |||||
if (!subgraph) { | |||||
subgraph = {}; | |||||
} | |||||
subgraph.parent = graph; | |||||
subgraph.node = graph.node; | |||||
subgraph.edge = graph.edge; | |||||
subgraph.graph = graph.graph; | |||||
// statements | |||||
parseStatements(subgraph); | |||||
// close angle bracket | |||||
if (token != '}') { | |||||
throw newSyntaxError('Angle bracket } expected'); | |||||
} | |||||
getToken(); | |||||
// remove temporary default properties | |||||
delete subgraph.node; | |||||
delete subgraph.edge; | |||||
delete subgraph.graph; | |||||
delete subgraph.parent; | |||||
// register at the parent graph | |||||
if (!graph.subgraphs) { | |||||
graph.subgraphs = []; | |||||
} | |||||
graph.subgraphs.push(subgraph); | |||||
} | |||||
return subgraph; | |||||
} | |||||
/** | |||||
* parse an attribute statement like "node [shape=circle fontSize=16]". | |||||
* Available keywords are 'node', 'edge', 'graph'. | |||||
* The previous list with default attributes will be replaced | |||||
* @param {Object} graph | |||||
* @returns {String | null} keyword Returns the name of the parsed attribute | |||||
* (node, edge, graph), or null if nothing | |||||
* is parsed. | |||||
*/ | |||||
function parseAttributeStatement (graph) { | |||||
// attribute statements | |||||
if (token == 'node') { | |||||
getToken(); | |||||
// node attributes | |||||
graph.node = parseAttributeList(); | |||||
return 'node'; | |||||
} | |||||
else if (token == 'edge') { | |||||
getToken(); | |||||
// edge attributes | |||||
graph.edge = parseAttributeList(); | |||||
return 'edge'; | |||||
} | |||||
else if (token == 'graph') { | |||||
getToken(); | |||||
// graph attributes | |||||
graph.graph = parseAttributeList(); | |||||
return 'graph'; | |||||
} | |||||
return null; | |||||
} | |||||
/** | |||||
* parse a node statement | |||||
* @param {Object} graph | |||||
* @param {String | Number} id | |||||
*/ | |||||
function parseNodeStatement(graph, id) { | |||||
// node statement | |||||
var node = { | |||||
id: id | |||||
}; | |||||
var attr = parseAttributeList(); | |||||
if (attr) { | |||||
node.attr = attr; | |||||
} | |||||
addNode(graph, node); | |||||
// edge statements | |||||
parseEdge(graph, id); | |||||
} | |||||
/** | |||||
* Parse an edge or a series of edges | |||||
* @param {Object} graph | |||||
* @param {String | Number} from Id of the from node | |||||
*/ | |||||
function parseEdge(graph, from) { | |||||
while (token == '->' || token == '--') { | |||||
var to; | |||||
var type = token; | |||||
getToken(); | |||||
var subgraph = parseSubgraph(graph); | |||||
if (subgraph) { | |||||
to = subgraph; | |||||
} | |||||
else { | |||||
if (tokenType != TOKENTYPE.IDENTIFIER) { | |||||
throw newSyntaxError('Identifier or subgraph expected'); | |||||
} | |||||
to = token; | |||||
addNode(graph, { | |||||
id: to | |||||
}); | |||||
getToken(); | |||||
} | |||||
// parse edge attributes | |||||
var attr = parseAttributeList(); | |||||
// create edge | |||||
var edge = createEdge(graph, from, to, type, attr); | |||||
addEdge(graph, edge); | |||||
from = to; | |||||
} | |||||
} | |||||
/** | |||||
* Parse a set with attributes, | |||||
* for example [label="1.000", shape=solid] | |||||
* @return {Object | null} attr | |||||
*/ | |||||
function parseAttributeList() { | |||||
var attr = null; | |||||
while (token == '[') { | |||||
getToken(); | |||||
attr = {}; | |||||
while (token !== '' && token != ']') { | |||||
if (tokenType != TOKENTYPE.IDENTIFIER) { | |||||
throw newSyntaxError('Attribute name expected'); | |||||
} | |||||
var name = token; | |||||
getToken(); | |||||
if (token != '=') { | |||||
throw newSyntaxError('Equal sign = expected'); | |||||
} | |||||
getToken(); | |||||
if (tokenType != TOKENTYPE.IDENTIFIER) { | |||||
throw newSyntaxError('Attribute value expected'); | |||||
} | |||||
var value = token; | |||||
setValue(attr, name, value); // name can be a path | |||||
getToken(); | |||||
if (token ==',') { | |||||
getToken(); | |||||
} | |||||
} | |||||
if (token != ']') { | |||||
throw newSyntaxError('Bracket ] expected'); | |||||
} | |||||
getToken(); | |||||
} | |||||
return attr; | |||||
} | |||||
/** | |||||
* Create a syntax error with extra information on current token and index. | |||||
* @param {String} message | |||||
* @returns {SyntaxError} err | |||||
*/ | |||||
function newSyntaxError(message) { | |||||
return new SyntaxError(message + ', got "' + chop(token, 30) + '" (char ' + index + ')'); | |||||
} | |||||
/** | |||||
* Chop off text after a maximum length | |||||
* @param {String} text | |||||
* @param {Number} maxLength | |||||
* @returns {String} | |||||
*/ | |||||
function chop (text, maxLength) { | |||||
return (text.length <= maxLength) ? text : (text.substr(0, 27) + '...'); | |||||
} | |||||
/** | |||||
* Execute a function fn for each pair of elements in two arrays | |||||
* @param {Array | *} array1 | |||||
* @param {Array | *} array2 | |||||
* @param {function} fn | |||||
*/ | |||||
function forEach2(array1, array2, fn) { | |||||
if (array1 instanceof Array) { | |||||
array1.forEach(function (elem1) { | |||||
if (array2 instanceof Array) { | |||||
array2.forEach(function (elem2) { | |||||
fn(elem1, elem2); | |||||
}); | |||||
} | |||||
else { | |||||
fn(elem1, array2); | |||||
} | |||||
}); | |||||
} | |||||
else { | |||||
if (array2 instanceof Array) { | |||||
array2.forEach(function (elem2) { | |||||
fn(array1, elem2); | |||||
}); | |||||
} | |||||
else { | |||||
fn(array1, array2); | |||||
} | |||||
} | |||||
} | |||||
/** | |||||
* Convert a string containing a graph in DOT language into a map containing | |||||
* with nodes and edges in the format of graph. | |||||
* @param {String} data Text containing a graph in DOT-notation | |||||
* @return {Object} graphData | |||||
*/ | |||||
function DOTToGraph (data) { | |||||
// parse the DOT file | |||||
var dotData = parseDOT(data); | |||||
var graphData = { | |||||
nodes: [], | |||||
edges: [], | |||||
options: {} | |||||
}; | |||||
// copy the nodes | |||||
if (dotData.nodes) { | |||||
dotData.nodes.forEach(function (dotNode) { | |||||
var graphNode = { | |||||
id: dotNode.id, | |||||
label: String(dotNode.label || dotNode.id) | |||||
}; | |||||
merge(graphNode, dotNode.attr); | |||||
if (graphNode.image) { | |||||
graphNode.shape = 'image'; | |||||
} | |||||
graphData.nodes.push(graphNode); | |||||
}); | |||||
} | |||||
// copy the edges | |||||
if (dotData.edges) { | |||||
/** | |||||
* Convert an edge in DOT format to an edge with VisGraph format | |||||
* @param {Object} dotEdge | |||||
* @returns {Object} graphEdge | |||||
*/ | |||||
function convertEdge(dotEdge) { | |||||
var graphEdge = { | |||||
from: dotEdge.from, | |||||
to: dotEdge.to | |||||
}; | |||||
merge(graphEdge, dotEdge.attr); | |||||
graphEdge.style = (dotEdge.type == '->') ? 'arrow' : 'line'; | |||||
return graphEdge; | |||||
} | |||||
dotData.edges.forEach(function (dotEdge) { | |||||
var from, to; | |||||
if (dotEdge.from instanceof Object) { | |||||
from = dotEdge.from.nodes; | |||||
} | |||||
else { | |||||
from = { | |||||
id: dotEdge.from | |||||
} | |||||
} | |||||
if (dotEdge.to instanceof Object) { | |||||
to = dotEdge.to.nodes; | |||||
} | |||||
else { | |||||
to = { | |||||
id: dotEdge.to | |||||
} | |||||
} | |||||
if (dotEdge.from instanceof Object && dotEdge.from.edges) { | |||||
dotEdge.from.edges.forEach(function (subEdge) { | |||||
var graphEdge = convertEdge(subEdge); | |||||
graphData.edges.push(graphEdge); | |||||
}); | |||||
} | |||||
forEach2(from, to, function (from, to) { | |||||
var subEdge = createEdge(graphData, from.id, to.id, dotEdge.type, dotEdge.attr); | |||||
var graphEdge = convertEdge(subEdge); | |||||
graphData.edges.push(graphEdge); | |||||
}); | |||||
if (dotEdge.to instanceof Object && dotEdge.to.edges) { | |||||
dotEdge.to.edges.forEach(function (subEdge) { | |||||
var graphEdge = convertEdge(subEdge); | |||||
graphData.edges.push(graphEdge); | |||||
}); | |||||
} | |||||
}); | |||||
} | |||||
// copy the options | |||||
if (dotData.attr) { | |||||
graphData.options = dotData.attr; | |||||
} | |||||
return graphData; | |||||
} | |||||
// exports | |||||
exports.parseDOT = parseDOT; | |||||
exports.DOTToGraph = DOTToGraph; | |||||
})(typeof util !== 'undefined' ? util : exports); |
@ -1,311 +0,0 @@ | |||||
var HierarchicalLayoutMixin = { | |||||
_resetLevels : function() { | |||||
for (var nodeId in this.nodes) { | |||||
if (this.nodes.hasOwnProperty(nodeId)) { | |||||
var node = this.nodes[nodeId]; | |||||
if (node.preassignedLevel == false) { | |||||
node.level = -1; | |||||
} | |||||
} | |||||
} | |||||
}, | |||||
/** | |||||
* This is the main function to layout the nodes in a hierarchical way. | |||||
* It checks if the node details are supplied correctly | |||||
* | |||||
* @private | |||||
*/ | |||||
_setupHierarchicalLayout : function() { | |||||
if (this.constants.hierarchicalLayout.enabled == true && this.nodeIndices.length > 0) { | |||||
if (this.constants.hierarchicalLayout.direction == "RL" || this.constants.hierarchicalLayout.direction == "DU") { | |||||
this.constants.hierarchicalLayout.levelSeparation *= -1; | |||||
} | |||||
else { | |||||
this.constants.hierarchicalLayout.levelSeparation = Math.abs(this.constants.hierarchicalLayout.levelSeparation); | |||||
} | |||||
// get the size of the largest hubs and check if the user has defined a level for a node. | |||||
var hubsize = 0; | |||||
var node, nodeId; | |||||
var definedLevel = false; | |||||
var undefinedLevel = false; | |||||
for (nodeId in this.nodes) { | |||||
if (this.nodes.hasOwnProperty(nodeId)) { | |||||
node = this.nodes[nodeId]; | |||||
if (node.level != -1) { | |||||
definedLevel = true; | |||||
} | |||||
else { | |||||
undefinedLevel = true; | |||||
} | |||||
if (hubsize < node.edges.length) { | |||||
hubsize = node.edges.length; | |||||
} | |||||
} | |||||
} | |||||
// if the user defined some levels but not all, alert and run without hierarchical layout | |||||
if (undefinedLevel == true && definedLevel == true) { | |||||
alert("To use the hierarchical layout, nodes require either no predefined levels or levels have to be defined for all nodes."); | |||||
this.zoomExtent(true,this.constants.clustering.enabled); | |||||
if (!this.constants.clustering.enabled) { | |||||
this.start(); | |||||
} | |||||
} | |||||
else { | |||||
// setup the system to use hierarchical method. | |||||
this._changeConstants(); | |||||
// define levels if undefined by the users. Based on hubsize | |||||
if (undefinedLevel == true) { | |||||
this._determineLevels(hubsize); | |||||
} | |||||
// check the distribution of the nodes per level. | |||||
var distribution = this._getDistribution(); | |||||
// place the nodes on the canvas. This also stablilizes the system. | |||||
this._placeNodesByHierarchy(distribution); | |||||
// start the simulation. | |||||
this.start(); | |||||
} | |||||
} | |||||
}, | |||||
/** | |||||
* This function places the nodes on the canvas based on the hierarchial distribution. | |||||
* | |||||
* @param {Object} distribution | obtained by the function this._getDistribution() | |||||
* @private | |||||
*/ | |||||
_placeNodesByHierarchy : function(distribution) { | |||||
var nodeId, node; | |||||
// start placing all the level 0 nodes first. Then recursively position their branches. | |||||
for (nodeId in distribution[0].nodes) { | |||||
if (distribution[0].nodes.hasOwnProperty(nodeId)) { | |||||
node = distribution[0].nodes[nodeId]; | |||||
if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") { | |||||
if (node.xFixed) { | |||||
node.x = distribution[0].minPos; | |||||
node.xFixed = false; | |||||
distribution[0].minPos += distribution[0].nodeSpacing; | |||||
} | |||||
} | |||||
else { | |||||
if (node.yFixed) { | |||||
node.y = distribution[0].minPos; | |||||
node.yFixed = false; | |||||
distribution[0].minPos += distribution[0].nodeSpacing; | |||||
} | |||||
} | |||||
this._placeBranchNodes(node.edges,node.id,distribution,node.level); | |||||
} | |||||
} | |||||
// stabilize the system after positioning. This function calls zoomExtent. | |||||
this._stabilize(); | |||||
}, | |||||
/** | |||||
* This function get the distribution of levels based on hubsize | |||||
* | |||||
* @returns {Object} | |||||
* @private | |||||
*/ | |||||
_getDistribution : function() { | |||||
var distribution = {}; | |||||
var nodeId, node, level; | |||||
// we fix Y because the hierarchy is vertical, we fix X so we do not give a node an x position for a second time. | |||||
// the fix of X is removed after the x value has been set. | |||||
for (nodeId in this.nodes) { | |||||
if (this.nodes.hasOwnProperty(nodeId)) { | |||||
node = this.nodes[nodeId]; | |||||
node.xFixed = true; | |||||
node.yFixed = true; | |||||
if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") { | |||||
node.y = this.constants.hierarchicalLayout.levelSeparation*node.level; | |||||
} | |||||
else { | |||||
node.x = this.constants.hierarchicalLayout.levelSeparation*node.level; | |||||
} | |||||
if (!distribution.hasOwnProperty(node.level)) { | |||||
distribution[node.level] = {amount: 0, nodes: {}, minPos:0, nodeSpacing:0}; | |||||
} | |||||
distribution[node.level].amount += 1; | |||||
distribution[node.level].nodes[node.id] = node; | |||||
} | |||||
} | |||||
// determine the largest amount of nodes of all levels | |||||
var maxCount = 0; | |||||
for (level in distribution) { | |||||
if (distribution.hasOwnProperty(level)) { | |||||
if (maxCount < distribution[level].amount) { | |||||
maxCount = distribution[level].amount; | |||||
} | |||||
} | |||||
} | |||||
// set the initial position and spacing of each nodes accordingly | |||||
for (level in distribution) { | |||||
if (distribution.hasOwnProperty(level)) { | |||||
distribution[level].nodeSpacing = (maxCount + 1) * this.constants.hierarchicalLayout.nodeSpacing; | |||||
distribution[level].nodeSpacing /= (distribution[level].amount + 1); | |||||
distribution[level].minPos = distribution[level].nodeSpacing - (0.5 * (distribution[level].amount + 1) * distribution[level].nodeSpacing); | |||||
} | |||||
} | |||||
return distribution; | |||||
}, | |||||
/** | |||||
* this function allocates nodes in levels based on the recursive branching from the largest hubs. | |||||
* | |||||
* @param hubsize | |||||
* @private | |||||
*/ | |||||
_determineLevels : function(hubsize) { | |||||
var nodeId, node; | |||||
// determine hubs | |||||
for (nodeId in this.nodes) { | |||||
if (this.nodes.hasOwnProperty(nodeId)) { | |||||
node = this.nodes[nodeId]; | |||||
if (node.edges.length == hubsize) { | |||||
node.level = 0; | |||||
} | |||||
} | |||||
} | |||||
// branch from hubs | |||||
for (nodeId in this.nodes) { | |||||
if (this.nodes.hasOwnProperty(nodeId)) { | |||||
node = this.nodes[nodeId]; | |||||
if (node.level == 0) { | |||||
this._setLevel(1,node.edges,node.id); | |||||
} | |||||
} | |||||
} | |||||
}, | |||||
/** | |||||
* Since hierarchical layout does not support: | |||||
* - smooth curves (based on the physics), | |||||
* - clustering (based on dynamic node counts) | |||||
* | |||||
* We disable both features so there will be no problems. | |||||
* | |||||
* @private | |||||
*/ | |||||
_changeConstants : function() { | |||||
this.constants.clustering.enabled = false; | |||||
this.constants.physics.barnesHut.enabled = false; | |||||
this.constants.physics.hierarchicalRepulsion.enabled = true; | |||||
this._loadSelectedForceSolver(); | |||||
this.constants.smoothCurves = false; | |||||
this._configureSmoothCurves(); | |||||
}, | |||||
/** | |||||
* This is a recursively called function to enumerate the branches from the largest hubs and place the nodes | |||||
* on a X position that ensures there will be no overlap. | |||||
* | |||||
* @param edges | |||||
* @param parentId | |||||
* @param distribution | |||||
* @param parentLevel | |||||
* @private | |||||
*/ | |||||
_placeBranchNodes : function(edges, parentId, distribution, parentLevel) { | |||||
for (var i = 0; i < edges.length; i++) { | |||||
var childNode = null; | |||||
if (edges[i].toId == parentId) { | |||||
childNode = edges[i].from; | |||||
} | |||||
else { | |||||
childNode = edges[i].to; | |||||
} | |||||
// if a node is conneceted to another node on the same level (or higher (means lower level))!, this is not handled here. | |||||
var nodeMoved = false; | |||||
if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") { | |||||
if (childNode.xFixed && childNode.level > parentLevel) { | |||||
childNode.xFixed = false; | |||||
childNode.x = distribution[childNode.level].minPos; | |||||
nodeMoved = true; | |||||
} | |||||
} | |||||
else { | |||||
if (childNode.yFixed && childNode.level > parentLevel) { | |||||
childNode.yFixed = false; | |||||
childNode.y = distribution[childNode.level].minPos; | |||||
nodeMoved = true; | |||||
} | |||||
} | |||||
if (nodeMoved == true) { | |||||
distribution[childNode.level].minPos += distribution[childNode.level].nodeSpacing; | |||||
if (childNode.edges.length > 1) { | |||||
this._placeBranchNodes(childNode.edges,childNode.id,distribution,childNode.level); | |||||
} | |||||
} | |||||
} | |||||
}, | |||||
/** | |||||
* this function is called recursively to enumerate the barnches of the largest hubs and give each node a level. | |||||
* | |||||
* @param level | |||||
* @param edges | |||||
* @param parentId | |||||
* @private | |||||
*/ | |||||
_setLevel : function(level, edges, parentId) { | |||||
for (var i = 0; i < edges.length; i++) { | |||||
var childNode = null; | |||||
if (edges[i].toId == parentId) { | |||||
childNode = edges[i].from; | |||||
} | |||||
else { | |||||
childNode = edges[i].to; | |||||
} | |||||
if (childNode.level == -1 || childNode.level > level) { | |||||
childNode.level = level; | |||||
if (edges.length > 1) { | |||||
this._setLevel(level+1, childNode.edges, childNode.id); | |||||
} | |||||
} | |||||
} | |||||
}, | |||||
/** | |||||
* Unfix nodes | |||||
* | |||||
* @private | |||||
*/ | |||||
_restoreNodes : function() { | |||||
for (nodeId in this.nodes) { | |||||
if (this.nodes.hasOwnProperty(nodeId)) { | |||||
this.nodes[nodeId].xFixed = false; | |||||
this.nodes[nodeId].yFixed = false; | |||||
} | |||||
} | |||||
} | |||||
}; |
@ -1,576 +0,0 @@ | |||||
/** | |||||
* Created by Alex on 2/4/14. | |||||
*/ | |||||
var manipulationMixin = { | |||||
/** | |||||
* clears the toolbar div element of children | |||||
* | |||||
* @private | |||||
*/ | |||||
_clearManipulatorBar : function() { | |||||
while (this.manipulationDiv.hasChildNodes()) { | |||||
this.manipulationDiv.removeChild(this.manipulationDiv.firstChild); | |||||
} | |||||
}, | |||||
/** | |||||
* Manipulation UI temporarily overloads certain functions to extend or replace them. To be able to restore | |||||
* these functions to their original functionality, we saved them in this.cachedFunctions. | |||||
* This function restores these functions to their original function. | |||||
* | |||||
* @private | |||||
*/ | |||||
_restoreOverloadedFunctions : function() { | |||||
for (var functionName in this.cachedFunctions) { | |||||
if (this.cachedFunctions.hasOwnProperty(functionName)) { | |||||
this[functionName] = this.cachedFunctions[functionName]; | |||||
} | |||||
} | |||||
}, | |||||
/** | |||||
* Enable or disable edit-mode. | |||||
* | |||||
* @private | |||||
*/ | |||||
_toggleEditMode : function() { | |||||
this.editMode = !this.editMode; | |||||
var toolbar = document.getElementById("network-manipulationDiv"); | |||||
var closeDiv = document.getElementById("network-manipulation-closeDiv"); | |||||
var editModeDiv = document.getElementById("network-manipulation-editMode"); | |||||
if (this.editMode == true) { | |||||
toolbar.style.display="block"; | |||||
closeDiv.style.display="block"; | |||||
editModeDiv.style.display="none"; | |||||
closeDiv.onclick = this._toggleEditMode.bind(this); | |||||
} | |||||
else { | |||||
toolbar.style.display="none"; | |||||
closeDiv.style.display="none"; | |||||
editModeDiv.style.display="block"; | |||||
closeDiv.onclick = null; | |||||
} | |||||
this._createManipulatorBar() | |||||
}, | |||||
/** | |||||
* main function, creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar. | |||||
* | |||||
* @private | |||||
*/ | |||||
_createManipulatorBar : function() { | |||||
// remove bound functions | |||||
if (this.boundFunction) { | |||||
this.off('select', this.boundFunction); | |||||
} | |||||
if (this.edgeBeingEdited !== undefined) { | |||||
this.edgeBeingEdited._disableControlNodes(); | |||||
this.edgeBeingEdited = undefined; | |||||
this.selectedControlNode = null; | |||||
} | |||||
// restore overloaded functions | |||||
this._restoreOverloadedFunctions(); | |||||
// resume calculation | |||||
this.freezeSimulation = false; | |||||
// reset global variables | |||||
this.blockConnectingEdgeSelection = false; | |||||
this.forceAppendSelection = false; | |||||
if (this.editMode == true) { | |||||
while (this.manipulationDiv.hasChildNodes()) { | |||||
this.manipulationDiv.removeChild(this.manipulationDiv.firstChild); | |||||
} | |||||
// add the icons to the manipulator div | |||||
this.manipulationDiv.innerHTML = "" + | |||||
"<span class='network-manipulationUI add' id='network-manipulate-addNode'>" + | |||||
"<span class='network-manipulationLabel'>"+this.constants.labels['add'] +"</span></span>" + | |||||
"<div class='network-seperatorLine'></div>" + | |||||
"<span class='network-manipulationUI connect' id='network-manipulate-connectNode'>" + | |||||
"<span class='network-manipulationLabel'>"+this.constants.labels['link'] +"</span></span>"; | |||||
if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) { | |||||
this.manipulationDiv.innerHTML += "" + | |||||
"<div class='network-seperatorLine'></div>" + | |||||
"<span class='network-manipulationUI edit' id='network-manipulate-editNode'>" + | |||||
"<span class='network-manipulationLabel'>"+this.constants.labels['editNode'] +"</span></span>"; | |||||
} | |||||
else if (this._getSelectedEdgeCount() == 1 && this._getSelectedNodeCount() == 0) { | |||||
this.manipulationDiv.innerHTML += "" + | |||||
"<div class='network-seperatorLine'></div>" + | |||||
"<span class='network-manipulationUI edit' id='network-manipulate-editEdge'>" + | |||||
"<span class='network-manipulationLabel'>"+this.constants.labels['editEdge'] +"</span></span>"; | |||||
} | |||||
if (this._selectionIsEmpty() == false) { | |||||
this.manipulationDiv.innerHTML += "" + | |||||
"<div class='network-seperatorLine'></div>" + | |||||
"<span class='network-manipulationUI delete' id='network-manipulate-delete'>" + | |||||
"<span class='network-manipulationLabel'>"+this.constants.labels['del'] +"</span></span>"; | |||||
} | |||||
// bind the icons | |||||
var addNodeButton = document.getElementById("network-manipulate-addNode"); | |||||
addNodeButton.onclick = this._createAddNodeToolbar.bind(this); | |||||
var addEdgeButton = document.getElementById("network-manipulate-connectNode"); | |||||
addEdgeButton.onclick = this._createAddEdgeToolbar.bind(this); | |||||
if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) { | |||||
var editButton = document.getElementById("network-manipulate-editNode"); | |||||
editButton.onclick = this._editNode.bind(this); | |||||
} | |||||
else if (this._getSelectedEdgeCount() == 1 && this._getSelectedNodeCount() == 0) { | |||||
var editButton = document.getElementById("network-manipulate-editEdge"); | |||||
editButton.onclick = this._createEditEdgeToolbar.bind(this); | |||||
} | |||||
if (this._selectionIsEmpty() == false) { | |||||
var deleteButton = document.getElementById("network-manipulate-delete"); | |||||
deleteButton.onclick = this._deleteSelected.bind(this); | |||||
} | |||||
var closeDiv = document.getElementById("network-manipulation-closeDiv"); | |||||
closeDiv.onclick = this._toggleEditMode.bind(this); | |||||
this.boundFunction = this._createManipulatorBar.bind(this); | |||||
this.on('select', this.boundFunction); | |||||
} | |||||
else { | |||||
this.editModeDiv.innerHTML = "" + | |||||
"<span class='network-manipulationUI edit editmode' id='network-manipulate-editModeButton'>" + | |||||
"<span class='network-manipulationLabel'>" + this.constants.labels['edit'] + "</span></span>"; | |||||
var editModeButton = document.getElementById("network-manipulate-editModeButton"); | |||||
editModeButton.onclick = this._toggleEditMode.bind(this); | |||||
} | |||||
}, | |||||
/** | |||||
* Create the toolbar for adding Nodes | |||||
* | |||||
* @private | |||||
*/ | |||||
_createAddNodeToolbar : function() { | |||||
// clear the toolbar | |||||
this._clearManipulatorBar(); | |||||
if (this.boundFunction) { | |||||
this.off('select', this.boundFunction); | |||||
} | |||||
// create the toolbar contents | |||||
this.manipulationDiv.innerHTML = "" + | |||||
"<span class='network-manipulationUI back' id='network-manipulate-back'>" + | |||||
"<span class='network-manipulationLabel'>" + this.constants.labels['back'] + " </span></span>" + | |||||
"<div class='network-seperatorLine'></div>" + | |||||
"<span class='network-manipulationUI none' id='network-manipulate-back'>" + | |||||
"<span id='network-manipulatorLabel' class='network-manipulationLabel'>" + this.constants.labels['addDescription'] + "</span></span>"; | |||||
// bind the icon | |||||
var backButton = document.getElementById("network-manipulate-back"); | |||||
backButton.onclick = this._createManipulatorBar.bind(this); | |||||
// we use the boundFunction so we can reference it when we unbind it from the "select" event. | |||||
this.boundFunction = this._addNode.bind(this); | |||||
this.on('select', this.boundFunction); | |||||
}, | |||||
/** | |||||
* create the toolbar to connect nodes | |||||
* | |||||
* @private | |||||
*/ | |||||
_createAddEdgeToolbar : function() { | |||||
// clear the toolbar | |||||
this._clearManipulatorBar(); | |||||
this._unselectAll(true); | |||||
this.freezeSimulation = true; | |||||
if (this.boundFunction) { | |||||
this.off('select', this.boundFunction); | |||||
} | |||||
this._unselectAll(); | |||||
this.forceAppendSelection = false; | |||||
this.blockConnectingEdgeSelection = true; | |||||
this.manipulationDiv.innerHTML = "" + | |||||
"<span class='network-manipulationUI back' id='network-manipulate-back'>" + | |||||
"<span class='network-manipulationLabel'>" + this.constants.labels['back'] + " </span></span>" + | |||||
"<div class='network-seperatorLine'></div>" + | |||||
"<span class='network-manipulationUI none' id='network-manipulate-back'>" + | |||||
"<span id='network-manipulatorLabel' class='network-manipulationLabel'>" + this.constants.labels['linkDescription'] + "</span></span>"; | |||||
// bind the icon | |||||
var backButton = document.getElementById("network-manipulate-back"); | |||||
backButton.onclick = this._createManipulatorBar.bind(this); | |||||
// we use the boundFunction so we can reference it when we unbind it from the "select" event. | |||||
this.boundFunction = this._handleConnect.bind(this); | |||||
this.on('select', this.boundFunction); | |||||
// temporarily overload functions | |||||
this.cachedFunctions["_handleTouch"] = this._handleTouch; | |||||
this.cachedFunctions["_handleOnRelease"] = this._handleOnRelease; | |||||
this._handleTouch = this._handleConnect; | |||||
this._handleOnRelease = this._finishConnect; | |||||
// redraw to show the unselect | |||||
this._redraw(); | |||||
}, | |||||
/** | |||||
* create the toolbar to edit edges | |||||
* | |||||
* @private | |||||
*/ | |||||
_createEditEdgeToolbar : function() { | |||||
// clear the toolbar | |||||
this._clearManipulatorBar(); | |||||
if (this.boundFunction) { | |||||
this.off('select', this.boundFunction); | |||||
} | |||||
this.edgeBeingEdited = this._getSelectedEdge(); | |||||
this.edgeBeingEdited._enableControlNodes(); | |||||
this.manipulationDiv.innerHTML = "" + | |||||
"<span class='network-manipulationUI back' id='network-manipulate-back'>" + | |||||
"<span class='network-manipulationLabel'>" + this.constants.labels['back'] + " </span></span>" + | |||||
"<div class='network-seperatorLine'></div>" + | |||||
"<span class='network-manipulationUI none' id='network-manipulate-back'>" + | |||||
"<span id='network-manipulatorLabel' class='network-manipulationLabel'>" + this.constants.labels['editEdgeDescription'] + "</span></span>"; | |||||
// bind the icon | |||||
var backButton = document.getElementById("network-manipulate-back"); | |||||
backButton.onclick = this._createManipulatorBar.bind(this); | |||||
// temporarily overload functions | |||||
this.cachedFunctions["_handleTouch"] = this._handleTouch; | |||||
this.cachedFunctions["_handleOnRelease"] = this._handleOnRelease; | |||||
this.cachedFunctions["_handleTap"] = this._handleTap; | |||||
this.cachedFunctions["_handleDragStart"] = this._handleDragStart; | |||||
this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag; | |||||
this._handleTouch = this._selectControlNode; | |||||
this._handleTap = function () {}; | |||||
this._handleOnDrag = this._controlNodeDrag; | |||||
this._handleDragStart = function () {} | |||||
this._handleOnRelease = this._releaseControlNode; | |||||
// redraw to show the unselect | |||||
this._redraw(); | |||||
}, | |||||
/** | |||||
* the function bound to the selection event. It checks if you want to connect a cluster and changes the description | |||||
* to walk the user through the process. | |||||
* | |||||
* @private | |||||
*/ | |||||
_selectControlNode : function(pointer) { | |||||
this.edgeBeingEdited.controlNodes.from.unselect(); | |||||
this.edgeBeingEdited.controlNodes.to.unselect(); | |||||
this.selectedControlNode = this.edgeBeingEdited._getSelectedControlNode(this._XconvertDOMtoCanvas(pointer.x),this._YconvertDOMtoCanvas(pointer.y)); | |||||
if (this.selectedControlNode !== null) { | |||||
this.selectedControlNode.select(); | |||||
this.freezeSimulation = true; | |||||
} | |||||
this._redraw(); | |||||
}, | |||||
/** | |||||
* the function bound to the selection event. It checks if you want to connect a cluster and changes the description | |||||
* to walk the user through the process. | |||||
* | |||||
* @private | |||||
*/ | |||||
_controlNodeDrag : function(event) { | |||||
var pointer = this._getPointer(event.gesture.center); | |||||
if (this.selectedControlNode !== null && this.selectedControlNode !== undefined) { | |||||
this.selectedControlNode.x = this._XconvertDOMtoCanvas(pointer.x); | |||||
this.selectedControlNode.y = this._YconvertDOMtoCanvas(pointer.y); | |||||
} | |||||
this._redraw(); | |||||
}, | |||||
_releaseControlNode : function(pointer) { | |||||
var newNode = this._getNodeAt(pointer); | |||||
if (newNode != null) { | |||||
if (this.edgeBeingEdited.controlNodes.from.selected == true) { | |||||
this._editEdge(newNode.id, this.edgeBeingEdited.to.id); | |||||
this.edgeBeingEdited.controlNodes.from.unselect(); | |||||
} | |||||
if (this.edgeBeingEdited.controlNodes.to.selected == true) { | |||||
this._editEdge(this.edgeBeingEdited.from.id, newNode.id); | |||||
this.edgeBeingEdited.controlNodes.to.unselect(); | |||||
} | |||||
} | |||||
else { | |||||
this.edgeBeingEdited._restoreControlNodes(); | |||||
} | |||||
this.freezeSimulation = false; | |||||
this._redraw(); | |||||
}, | |||||
/** | |||||
* the function bound to the selection event. It checks if you want to connect a cluster and changes the description | |||||
* to walk the user through the process. | |||||
* | |||||
* @private | |||||
*/ | |||||
_handleConnect : function(pointer) { | |||||
if (this._getSelectedNodeCount() == 0) { | |||||
var node = this._getNodeAt(pointer); | |||||
if (node != null) { | |||||
if (node.clusterSize > 1) { | |||||
alert("Cannot create edges to a cluster.") | |||||
} | |||||
else { | |||||
this._selectObject(node,false); | |||||
// create a node the temporary line can look at | |||||
this.sectors['support']['nodes']['targetNode'] = new Node({id:'targetNode'},{},{},this.constants); | |||||
this.sectors['support']['nodes']['targetNode'].x = node.x; | |||||
this.sectors['support']['nodes']['targetNode'].y = node.y; | |||||
this.sectors['support']['nodes']['targetViaNode'] = new Node({id:'targetViaNode'},{},{},this.constants); | |||||
this.sectors['support']['nodes']['targetViaNode'].x = node.x; | |||||
this.sectors['support']['nodes']['targetViaNode'].y = node.y; | |||||
this.sectors['support']['nodes']['targetViaNode'].parentEdgeId = "connectionEdge"; | |||||
// create a temporary edge | |||||
this.edges['connectionEdge'] = new Edge({id:"connectionEdge",from:node.id,to:this.sectors['support']['nodes']['targetNode'].id}, this, this.constants); | |||||
this.edges['connectionEdge'].from = node; | |||||
this.edges['connectionEdge'].connected = true; | |||||
this.edges['connectionEdge'].smooth = true; | |||||
this.edges['connectionEdge'].selected = true; | |||||
this.edges['connectionEdge'].to = this.sectors['support']['nodes']['targetNode']; | |||||
this.edges['connectionEdge'].via = this.sectors['support']['nodes']['targetViaNode']; | |||||
this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag; | |||||
this._handleOnDrag = function(event) { | |||||
var pointer = this._getPointer(event.gesture.center); | |||||
this.sectors['support']['nodes']['targetNode'].x = this._XconvertDOMtoCanvas(pointer.x); | |||||
this.sectors['support']['nodes']['targetNode'].y = this._YconvertDOMtoCanvas(pointer.y); | |||||
this.sectors['support']['nodes']['targetViaNode'].x = 0.5 * (this._XconvertDOMtoCanvas(pointer.x) + this.edges['connectionEdge'].from.x); | |||||
this.sectors['support']['nodes']['targetViaNode'].y = this._YconvertDOMtoCanvas(pointer.y); | |||||
}; | |||||
this.moving = true; | |||||
this.start(); | |||||
} | |||||
} | |||||
} | |||||
}, | |||||
_finishConnect : function(pointer) { | |||||
if (this._getSelectedNodeCount() == 1) { | |||||
// restore the drag function | |||||
this._handleOnDrag = this.cachedFunctions["_handleOnDrag"]; | |||||
delete this.cachedFunctions["_handleOnDrag"]; | |||||
// remember the edge id | |||||
var connectFromId = this.edges['connectionEdge'].fromId; | |||||
// remove the temporary nodes and edge | |||||
delete this.edges['connectionEdge']; | |||||
delete this.sectors['support']['nodes']['targetNode']; | |||||
delete this.sectors['support']['nodes']['targetViaNode']; | |||||
var node = this._getNodeAt(pointer); | |||||
if (node != null) { | |||||
if (node.clusterSize > 1) { | |||||
alert("Cannot create edges to a cluster.") | |||||
} | |||||
else { | |||||
this._createEdge(connectFromId,node.id); | |||||
this._createManipulatorBar(); | |||||
} | |||||
} | |||||
this._unselectAll(); | |||||
} | |||||
}, | |||||
/** | |||||
* Adds a node on the specified location | |||||
*/ | |||||
_addNode : function() { | |||||
if (this._selectionIsEmpty() && this.editMode == true) { | |||||
var positionObject = this._pointerToPositionObject(this.pointerPosition); | |||||
var defaultData = {id:util.randomUUID(),x:positionObject.left,y:positionObject.top,label:"new",allowedToMoveX:true,allowedToMoveY:true}; | |||||
if (this.triggerFunctions.add) { | |||||
if (this.triggerFunctions.add.length == 2) { | |||||
var me = this; | |||||
this.triggerFunctions.add(defaultData, function(finalizedData) { | |||||
me.nodesData.add(finalizedData); | |||||
me._createManipulatorBar(); | |||||
me.moving = true; | |||||
me.start(); | |||||
}); | |||||
} | |||||
else { | |||||
alert(this.constants.labels['addError']); | |||||
this._createManipulatorBar(); | |||||
this.moving = true; | |||||
this.start(); | |||||
} | |||||
} | |||||
else { | |||||
this.nodesData.add(defaultData); | |||||
this._createManipulatorBar(); | |||||
this.moving = true; | |||||
this.start(); | |||||
} | |||||
} | |||||
}, | |||||
/** | |||||
* connect two nodes with a new edge. | |||||
* | |||||
* @private | |||||
*/ | |||||
_createEdge : function(sourceNodeId,targetNodeId) { | |||||
if (this.editMode == true) { | |||||
var defaultData = {from:sourceNodeId, to:targetNodeId}; | |||||
if (this.triggerFunctions.connect) { | |||||
if (this.triggerFunctions.connect.length == 2) { | |||||
var me = this; | |||||
this.triggerFunctions.connect(defaultData, function(finalizedData) { | |||||
me.edgesData.add(finalizedData); | |||||
me.moving = true; | |||||
me.start(); | |||||
}); | |||||
} | |||||
else { | |||||
alert(this.constants.labels["linkError"]); | |||||
this.moving = true; | |||||
this.start(); | |||||
} | |||||
} | |||||
else { | |||||
this.edgesData.add(defaultData); | |||||
this.moving = true; | |||||
this.start(); | |||||
} | |||||
} | |||||
}, | |||||
/** | |||||
* connect two nodes with a new edge. | |||||
* | |||||
* @private | |||||
*/ | |||||
_editEdge : function(sourceNodeId,targetNodeId) { | |||||
if (this.editMode == true) { | |||||
var defaultData = {id: this.edgeBeingEdited.id, from:sourceNodeId, to:targetNodeId}; | |||||
if (this.triggerFunctions.editEdge) { | |||||
if (this.triggerFunctions.editEdge.length == 2) { | |||||
var me = this; | |||||
this.triggerFunctions.editEdge(defaultData, function(finalizedData) { | |||||
me.edgesData.update(finalizedData); | |||||
me.moving = true; | |||||
me.start(); | |||||
}); | |||||
} | |||||
else { | |||||
alert(this.constants.labels["linkError"]); | |||||
this.moving = true; | |||||
this.start(); | |||||
} | |||||
} | |||||
else { | |||||
this.edgesData.update(defaultData); | |||||
this.moving = true; | |||||
this.start(); | |||||
} | |||||
} | |||||
}, | |||||
/** | |||||
* Create the toolbar to edit the selected node. The label and the color can be changed. Other colors are derived from the chosen color. | |||||
* | |||||
* @private | |||||
*/ | |||||
_editNode : function() { | |||||
if (this.triggerFunctions.edit && this.editMode == true) { | |||||
var node = this._getSelectedNode(); | |||||
var data = {id:node.id, | |||||
label: node.label, | |||||
group: node.group, | |||||
shape: node.shape, | |||||
color: { | |||||
background:node.color.background, | |||||
border:node.color.border, | |||||
highlight: { | |||||
background:node.color.highlight.background, | |||||
border:node.color.highlight.border | |||||
} | |||||
}}; | |||||
if (this.triggerFunctions.edit.length == 2) { | |||||
var me = this; | |||||
this.triggerFunctions.edit(data, function (finalizedData) { | |||||
me.nodesData.update(finalizedData); | |||||
me._createManipulatorBar(); | |||||
me.moving = true; | |||||
me.start(); | |||||
}); | |||||
} | |||||
else { | |||||
alert(this.constants.labels["editError"]); | |||||
} | |||||
} | |||||
else { | |||||
alert(this.constants.labels["editBoundError"]); | |||||
} | |||||
}, | |||||
/** | |||||
* delete everything in the selection | |||||
* | |||||
* @private | |||||
*/ | |||||
_deleteSelected : function() { | |||||
if (!this._selectionIsEmpty() && this.editMode == true) { | |||||
if (!this._clusterInSelection()) { | |||||
var selectedNodes = this.getSelectedNodes(); | |||||
var selectedEdges = this.getSelectedEdges(); | |||||
if (this.triggerFunctions.del) { | |||||
var me = this; | |||||
var data = {nodes: selectedNodes, edges: selectedEdges}; | |||||
if (this.triggerFunctions.del.length = 2) { | |||||
this.triggerFunctions.del(data, function (finalizedData) { | |||||
me.edgesData.remove(finalizedData.edges); | |||||
me.nodesData.remove(finalizedData.nodes); | |||||
me._unselectAll(); | |||||
me.moving = true; | |||||
me.start(); | |||||
}); | |||||
} | |||||
else { | |||||
alert(this.constants.labels["deleteError"]) | |||||
} | |||||
} | |||||
else { | |||||
this.edgesData.remove(selectedEdges); | |||||
this.nodesData.remove(selectedNodes); | |||||
this._unselectAll(); | |||||
this.moving = true; | |||||
this.start(); | |||||
} | |||||
} | |||||
else { | |||||
alert(this.constants.labels["deleteClusterError"]); | |||||
} | |||||
} | |||||
} | |||||
}; |
@ -1,199 +0,0 @@ | |||||
/** | |||||
* Created by Alex on 2/10/14. | |||||
*/ | |||||
var networkMixinLoaders = { | |||||
/** | |||||
* Load a mixin into the network object | |||||
* | |||||
* @param {Object} sourceVariable | this object has to contain functions. | |||||
* @private | |||||
*/ | |||||
_loadMixin: function (sourceVariable) { | |||||
for (var mixinFunction in sourceVariable) { | |||||
if (sourceVariable.hasOwnProperty(mixinFunction)) { | |||||
Network.prototype[mixinFunction] = sourceVariable[mixinFunction]; | |||||
} | |||||
} | |||||
}, | |||||
/** | |||||
* removes a mixin from the network object. | |||||
* | |||||
* @param {Object} sourceVariable | this object has to contain functions. | |||||
* @private | |||||
*/ | |||||
_clearMixin: function (sourceVariable) { | |||||
for (var mixinFunction in sourceVariable) { | |||||
if (sourceVariable.hasOwnProperty(mixinFunction)) { | |||||
Network.prototype[mixinFunction] = undefined; | |||||
} | |||||
} | |||||
}, | |||||
/** | |||||
* Mixin the physics system and initialize the parameters required. | |||||
* | |||||
* @private | |||||
*/ | |||||
_loadPhysicsSystem: function () { | |||||
this._loadMixin(physicsMixin); | |||||
this._loadSelectedForceSolver(); | |||||
if (this.constants.configurePhysics == true) { | |||||
this._loadPhysicsConfiguration(); | |||||
} | |||||
}, | |||||
/** | |||||
* Mixin the cluster system and initialize the parameters required. | |||||
* | |||||
* @private | |||||
*/ | |||||
_loadClusterSystem: function () { | |||||
this.clusterSession = 0; | |||||
this.hubThreshold = 5; | |||||
this._loadMixin(ClusterMixin); | |||||
}, | |||||
/** | |||||
* Mixin the sector system and initialize the parameters required | |||||
* | |||||
* @private | |||||
*/ | |||||
_loadSectorSystem: function () { | |||||
this.sectors = {}; | |||||
this.activeSector = ["default"]; | |||||
this.sectors["active"] = {}; | |||||
this.sectors["active"]["default"] = {"nodes": {}, | |||||
"edges": {}, | |||||
"nodeIndices": [], | |||||
"formationScale": 1.0, | |||||
"drawingNode": undefined }; | |||||
this.sectors["frozen"] = {}; | |||||
this.sectors["support"] = {"nodes": {}, | |||||
"edges": {}, | |||||
"nodeIndices": [], | |||||
"formationScale": 1.0, | |||||
"drawingNode": undefined }; | |||||
this.nodeIndices = this.sectors["active"]["default"]["nodeIndices"]; // the node indices list is used to speed up the computation of the repulsion fields | |||||
this._loadMixin(SectorMixin); | |||||
}, | |||||
/** | |||||
* Mixin the selection system and initialize the parameters required | |||||
* | |||||
* @private | |||||
*/ | |||||
_loadSelectionSystem: function () { | |||||
this.selectionObj = {nodes: {}, edges: {}}; | |||||
this._loadMixin(SelectionMixin); | |||||
}, | |||||
/** | |||||
* Mixin the navigationUI (User Interface) system and initialize the parameters required | |||||
* | |||||
* @private | |||||
*/ | |||||
_loadManipulationSystem: function () { | |||||
// reset global variables -- these are used by the selection of nodes and edges. | |||||
this.blockConnectingEdgeSelection = false; | |||||
this.forceAppendSelection = false; | |||||
if (this.constants.dataManipulation.enabled == true) { | |||||
// load the manipulator HTML elements. All styling done in css. | |||||
if (this.manipulationDiv === undefined) { | |||||
this.manipulationDiv = document.createElement('div'); | |||||
this.manipulationDiv.className = 'network-manipulationDiv'; | |||||
this.manipulationDiv.id = 'network-manipulationDiv'; | |||||
if (this.editMode == true) { | |||||
this.manipulationDiv.style.display = "block"; | |||||
} | |||||
else { | |||||
this.manipulationDiv.style.display = "none"; | |||||
} | |||||
this.containerElement.insertBefore(this.manipulationDiv, this.frame); | |||||
} | |||||
if (this.editModeDiv === undefined) { | |||||
this.editModeDiv = document.createElement('div'); | |||||
this.editModeDiv.className = 'network-manipulation-editMode'; | |||||
this.editModeDiv.id = 'network-manipulation-editMode'; | |||||
if (this.editMode == true) { | |||||
this.editModeDiv.style.display = "none"; | |||||
} | |||||
else { | |||||
this.editModeDiv.style.display = "block"; | |||||
} | |||||
this.containerElement.insertBefore(this.editModeDiv, this.frame); | |||||
} | |||||
if (this.closeDiv === undefined) { | |||||
this.closeDiv = document.createElement('div'); | |||||
this.closeDiv.className = 'network-manipulation-closeDiv'; | |||||
this.closeDiv.id = 'network-manipulation-closeDiv'; | |||||
this.closeDiv.style.display = this.manipulationDiv.style.display; | |||||
this.containerElement.insertBefore(this.closeDiv, this.frame); | |||||
} | |||||
// load the manipulation functions | |||||
this._loadMixin(manipulationMixin); | |||||
// create the manipulator toolbar | |||||
this._createManipulatorBar(); | |||||
} | |||||
else { | |||||
if (this.manipulationDiv !== undefined) { | |||||
// removes all the bindings and overloads | |||||
this._createManipulatorBar(); | |||||
// remove the manipulation divs | |||||
this.containerElement.removeChild(this.manipulationDiv); | |||||
this.containerElement.removeChild(this.editModeDiv); | |||||
this.containerElement.removeChild(this.closeDiv); | |||||
this.manipulationDiv = undefined; | |||||
this.editModeDiv = undefined; | |||||
this.closeDiv = undefined; | |||||
// remove the mixin functions | |||||
this._clearMixin(manipulationMixin); | |||||
} | |||||
} | |||||
}, | |||||
/** | |||||
* Mixin the navigation (User Interface) system and initialize the parameters required | |||||
* | |||||
* @private | |||||
*/ | |||||
_loadNavigationControls: function () { | |||||
this._loadMixin(NavigationMixin); | |||||
// the clean function removes the button divs, this is done to remove the bindings. | |||||
this._cleanNavigation(); | |||||
if (this.constants.navigation.enabled == true) { | |||||
this._loadNavigationElements(); | |||||
} | |||||
}, | |||||
/** | |||||
* Mixin the hierarchical layout system. | |||||
* | |||||
* @private | |||||
*/ | |||||
_loadHierarchySystem: function () { | |||||
this._loadMixin(HierarchicalLayoutMixin); | |||||
} | |||||
}; |
@ -1,205 +0,0 @@ | |||||
/** | |||||
* Created by Alex on 1/22/14. | |||||
*/ | |||||
var NavigationMixin = { | |||||
_cleanNavigation : function() { | |||||
// clean up previosu navigation items | |||||
var wrapper = document.getElementById('network-navigation_wrapper'); | |||||
if (wrapper != null) { | |||||
this.containerElement.removeChild(wrapper); | |||||
} | |||||
document.onmouseup = null; | |||||
}, | |||||
/** | |||||
* Creation of the navigation controls nodes. They are drawn over the rest of the nodes and are not affected by scale and translation | |||||
* they have a triggerFunction which is called on click. If the position of the navigation controls is dependent | |||||
* on this.frame.canvas.clientWidth or this.frame.canvas.clientHeight, we flag horizontalAlignLeft and verticalAlignTop false. | |||||
* This means that the location will be corrected by the _relocateNavigation function on a size change of the canvas. | |||||
* | |||||
* @private | |||||
*/ | |||||
_loadNavigationElements : function() { | |||||
this._cleanNavigation(); | |||||
this.navigationDivs = {}; | |||||
var navigationDivs = ['up','down','left','right','zoomIn','zoomOut','zoomExtends']; | |||||
var navigationDivActions = ['_moveUp','_moveDown','_moveLeft','_moveRight','_zoomIn','_zoomOut','zoomExtent']; | |||||
this.navigationDivs['wrapper'] = document.createElement('div'); | |||||
this.navigationDivs['wrapper'].id = "network-navigation_wrapper"; | |||||
this.navigationDivs['wrapper'].style.position = "absolute"; | |||||
this.navigationDivs['wrapper'].style.width = this.frame.canvas.clientWidth + "px"; | |||||
this.navigationDivs['wrapper'].style.height = this.frame.canvas.clientHeight + "px"; | |||||
this.containerElement.insertBefore(this.navigationDivs['wrapper'],this.frame); | |||||
for (var i = 0; i < navigationDivs.length; i++) { | |||||
this.navigationDivs[navigationDivs[i]] = document.createElement('div'); | |||||
this.navigationDivs[navigationDivs[i]].id = "network-navigation_" + navigationDivs[i]; | |||||
this.navigationDivs[navigationDivs[i]].className = "network-navigation " + navigationDivs[i]; | |||||
this.navigationDivs['wrapper'].appendChild(this.navigationDivs[navigationDivs[i]]); | |||||
this.navigationDivs[navigationDivs[i]].onmousedown = this[navigationDivActions[i]].bind(this); | |||||
} | |||||
document.onmouseup = this._stopMovement.bind(this); | |||||
}, | |||||
/** | |||||
* this stops all movement induced by the navigation buttons | |||||
* | |||||
* @private | |||||
*/ | |||||
_stopMovement : function() { | |||||
this._xStopMoving(); | |||||
this._yStopMoving(); | |||||
this._stopZoom(); | |||||
}, | |||||
/** | |||||
* stops the actions performed by page up and down etc. | |||||
* | |||||
* @param event | |||||
* @private | |||||
*/ | |||||
_preventDefault : function(event) { | |||||
if (event !== undefined) { | |||||
if (event.preventDefault) { | |||||
event.preventDefault(); | |||||
} else { | |||||
event.returnValue = false; | |||||
} | |||||
} | |||||
}, | |||||
/** | |||||
* move the screen up | |||||
* By using the increments, instead of adding a fixed number to the translation, we keep fluent and | |||||
* instant movement. The onKeypress event triggers immediately, then pauses, then triggers frequently | |||||
* To avoid this behaviour, we do the translation in the start loop. | |||||
* | |||||
* @private | |||||
*/ | |||||
_moveUp : function(event) { | |||||
this.yIncrement = this.constants.keyboard.speed.y; | |||||
this.start(); // if there is no node movement, the calculation wont be done | |||||
this._preventDefault(event); | |||||
if (this.navigationDivs) { | |||||
this.navigationDivs['up'].className += " active"; | |||||
} | |||||
}, | |||||
/** | |||||
* move the screen down | |||||
* @private | |||||
*/ | |||||
_moveDown : function(event) { | |||||
this.yIncrement = -this.constants.keyboard.speed.y; | |||||
this.start(); // if there is no node movement, the calculation wont be done | |||||
this._preventDefault(event); | |||||
if (this.navigationDivs) { | |||||
this.navigationDivs['down'].className += " active"; | |||||
} | |||||
}, | |||||
/** | |||||
* move the screen left | |||||
* @private | |||||
*/ | |||||
_moveLeft : function(event) { | |||||
this.xIncrement = this.constants.keyboard.speed.x; | |||||
this.start(); // if there is no node movement, the calculation wont be done | |||||
this._preventDefault(event); | |||||
if (this.navigationDivs) { | |||||
this.navigationDivs['left'].className += " active"; | |||||
} | |||||
}, | |||||
/** | |||||
* move the screen right | |||||
* @private | |||||
*/ | |||||
_moveRight : function(event) { | |||||
this.xIncrement = -this.constants.keyboard.speed.y; | |||||
this.start(); // if there is no node movement, the calculation wont be done | |||||
this._preventDefault(event); | |||||
if (this.navigationDivs) { | |||||
this.navigationDivs['right'].className += " active"; | |||||
} | |||||
}, | |||||
/** | |||||
* Zoom in, using the same method as the movement. | |||||
* @private | |||||
*/ | |||||
_zoomIn : function(event) { | |||||
this.zoomIncrement = this.constants.keyboard.speed.zoom; | |||||
this.start(); // if there is no node movement, the calculation wont be done | |||||
this._preventDefault(event); | |||||
if (this.navigationDivs) { | |||||
this.navigationDivs['zoomIn'].className += " active"; | |||||
} | |||||
}, | |||||
/** | |||||
* Zoom out | |||||
* @private | |||||
*/ | |||||
_zoomOut : function() { | |||||
this.zoomIncrement = -this.constants.keyboard.speed.zoom; | |||||
this.start(); // if there is no node movement, the calculation wont be done | |||||
this._preventDefault(event); | |||||
if (this.navigationDivs) { | |||||
this.navigationDivs['zoomOut'].className += " active"; | |||||
} | |||||
}, | |||||
/** | |||||
* Stop zooming and unhighlight the zoom controls | |||||
* @private | |||||
*/ | |||||
_stopZoom : function() { | |||||
this.zoomIncrement = 0; | |||||
if (this.navigationDivs) { | |||||
this.navigationDivs['zoomIn'].className = this.navigationDivs['zoomIn'].className.replace(" active",""); | |||||
this.navigationDivs['zoomOut'].className = this.navigationDivs['zoomOut'].className.replace(" active",""); | |||||
} | |||||
}, | |||||
/** | |||||
* Stop moving in the Y direction and unHighlight the up and down | |||||
* @private | |||||
*/ | |||||
_yStopMoving : function() { | |||||
this.yIncrement = 0; | |||||
if (this.navigationDivs) { | |||||
this.navigationDivs['up'].className = this.navigationDivs['up'].className.replace(" active",""); | |||||
this.navigationDivs['down'].className = this.navigationDivs['down'].className.replace(" active",""); | |||||
} | |||||
}, | |||||
/** | |||||
* Stop moving in the X direction and unHighlight left and right. | |||||
* @private | |||||
*/ | |||||
_xStopMoving : function() { | |||||
this.xIncrement = 0; | |||||
if (this.navigationDivs) { | |||||
this.navigationDivs['left'].className = this.navigationDivs['left'].className.replace(" active",""); | |||||
this.navigationDivs['right'].className = this.navigationDivs['right'].className.replace(" active",""); | |||||
} | |||||
} | |||||
}; |
@ -1,552 +0,0 @@ | |||||
/** | |||||
* Creation of the SectorMixin var. | |||||
* | |||||
* This contains all the functions the Network object can use to employ the sector system. | |||||
* The sector system is always used by Network, though the benefits only apply to the use of clustering. | |||||
* If clustering is not used, there is no overhead except for a duplicate object with references to nodes and edges. | |||||
* | |||||
* Alex de Mulder | |||||
* 21-01-2013 | |||||
*/ | |||||
var SectorMixin = { | |||||
/** | |||||
* This function is only called by the setData function of the Network object. | |||||
* This loads the global references into the active sector. This initializes the sector. | |||||
* | |||||
* @private | |||||
*/ | |||||
_putDataInSector : function() { | |||||
this.sectors["active"][this._sector()].nodes = this.nodes; | |||||
this.sectors["active"][this._sector()].edges = this.edges; | |||||
this.sectors["active"][this._sector()].nodeIndices = this.nodeIndices; | |||||
}, | |||||
/** | |||||
* /** | |||||
* This function sets the global references to nodes, edges and nodeIndices back to | |||||
* those of the supplied (active) sector. If a type is defined, do the specific type | |||||
* | |||||
* @param {String} sectorId | |||||
* @param {String} [sectorType] | "active" or "frozen" | |||||
* @private | |||||
*/ | |||||
_switchToSector : function(sectorId, sectorType) { | |||||
if (sectorType === undefined || sectorType == "active") { | |||||
this._switchToActiveSector(sectorId); | |||||
} | |||||
else { | |||||
this._switchToFrozenSector(sectorId); | |||||
} | |||||
}, | |||||
/** | |||||
* This function sets the global references to nodes, edges and nodeIndices back to | |||||
* those of the supplied active sector. | |||||
* | |||||
* @param sectorId | |||||
* @private | |||||
*/ | |||||
_switchToActiveSector : function(sectorId) { | |||||
this.nodeIndices = this.sectors["active"][sectorId]["nodeIndices"]; | |||||
this.nodes = this.sectors["active"][sectorId]["nodes"]; | |||||
this.edges = this.sectors["active"][sectorId]["edges"]; | |||||
}, | |||||
/** | |||||
* This function sets the global references to nodes, edges and nodeIndices back to | |||||
* those of the supplied active sector. | |||||
* | |||||
* @param sectorId | |||||
* @private | |||||
*/ | |||||
_switchToSupportSector : function() { | |||||
this.nodeIndices = this.sectors["support"]["nodeIndices"]; | |||||
this.nodes = this.sectors["support"]["nodes"]; | |||||
this.edges = this.sectors["support"]["edges"]; | |||||
}, | |||||
/** | |||||
* This function sets the global references to nodes, edges and nodeIndices back to | |||||
* those of the supplied frozen sector. | |||||
* | |||||
* @param sectorId | |||||
* @private | |||||
*/ | |||||
_switchToFrozenSector : function(sectorId) { | |||||
this.nodeIndices = this.sectors["frozen"][sectorId]["nodeIndices"]; | |||||
this.nodes = this.sectors["frozen"][sectorId]["nodes"]; | |||||
this.edges = this.sectors["frozen"][sectorId]["edges"]; | |||||
}, | |||||
/** | |||||
* This function sets the global references to nodes, edges and nodeIndices back to | |||||
* those of the currently active sector. | |||||
* | |||||
* @private | |||||
*/ | |||||
_loadLatestSector : function() { | |||||
this._switchToSector(this._sector()); | |||||
}, | |||||
/** | |||||
* This function returns the currently active sector Id | |||||
* | |||||
* @returns {String} | |||||
* @private | |||||
*/ | |||||
_sector : function() { | |||||
return this.activeSector[this.activeSector.length-1]; | |||||
}, | |||||
/** | |||||
* This function returns the previously active sector Id | |||||
* | |||||
* @returns {String} | |||||
* @private | |||||
*/ | |||||
_previousSector : function() { | |||||
if (this.activeSector.length > 1) { | |||||
return this.activeSector[this.activeSector.length-2]; | |||||
} | |||||
else { | |||||
throw new TypeError('there are not enough sectors in the this.activeSector array.'); | |||||
} | |||||
}, | |||||
/** | |||||
* We add the active sector at the end of the this.activeSector array | |||||
* This ensures it is the currently active sector returned by _sector() and it reaches the top | |||||
* of the activeSector stack. When we reverse our steps we move from the end to the beginning of this stack. | |||||
* | |||||
* @param newId | |||||
* @private | |||||
*/ | |||||
_setActiveSector : function(newId) { | |||||
this.activeSector.push(newId); | |||||
}, | |||||
/** | |||||
* We remove the currently active sector id from the active sector stack. This happens when | |||||
* we reactivate the previously active sector | |||||
* | |||||
* @private | |||||
*/ | |||||
_forgetLastSector : function() { | |||||
this.activeSector.pop(); | |||||
}, | |||||
/** | |||||
* This function creates a new active sector with the supplied newId. This newId | |||||
* is the expanding node id. | |||||
* | |||||
* @param {String} newId | Id of the new active sector | |||||
* @private | |||||
*/ | |||||
_createNewSector : function(newId) { | |||||
// create the new sector | |||||
this.sectors["active"][newId] = {"nodes":{}, | |||||
"edges":{}, | |||||
"nodeIndices":[], | |||||
"formationScale": this.scale, | |||||
"drawingNode": undefined}; | |||||
// create the new sector render node. This gives visual feedback that you are in a new sector. | |||||
this.sectors["active"][newId]['drawingNode'] = new Node( | |||||
{id:newId, | |||||
color: { | |||||
background: "#eaefef", | |||||
border: "495c5e" | |||||
} | |||||
},{},{},this.constants); | |||||
this.sectors["active"][newId]['drawingNode'].clusterSize = 2; | |||||
}, | |||||
/** | |||||
* This function removes the currently active sector. This is called when we create a new | |||||
* active sector. | |||||
* | |||||
* @param {String} sectorId | Id of the active sector that will be removed | |||||
* @private | |||||
*/ | |||||
_deleteActiveSector : function(sectorId) { | |||||
delete this.sectors["active"][sectorId]; | |||||
}, | |||||
/** | |||||
* This function removes the currently active sector. This is called when we reactivate | |||||
* the previously active sector. | |||||
* | |||||
* @param {String} sectorId | Id of the active sector that will be removed | |||||
* @private | |||||
*/ | |||||
_deleteFrozenSector : function(sectorId) { | |||||
delete this.sectors["frozen"][sectorId]; | |||||
}, | |||||
/** | |||||
* Freezing an active sector means moving it from the "active" object to the "frozen" object. | |||||
* We copy the references, then delete the active entree. | |||||
* | |||||
* @param sectorId | |||||
* @private | |||||
*/ | |||||
_freezeSector : function(sectorId) { | |||||
// we move the set references from the active to the frozen stack. | |||||
this.sectors["frozen"][sectorId] = this.sectors["active"][sectorId]; | |||||
// we have moved the sector data into the frozen set, we now remove it from the active set | |||||
this._deleteActiveSector(sectorId); | |||||
}, | |||||
/** | |||||
* This is the reverse operation of _freezeSector. Activating means moving the sector from the "frozen" | |||||
* object to the "active" object. | |||||
* | |||||
* @param sectorId | |||||
* @private | |||||
*/ | |||||
_activateSector : function(sectorId) { | |||||
// we move the set references from the frozen to the active stack. | |||||
this.sectors["active"][sectorId] = this.sectors["frozen"][sectorId]; | |||||
// we have moved the sector data into the active set, we now remove it from the frozen stack | |||||
this._deleteFrozenSector(sectorId); | |||||
}, | |||||
/** | |||||
* This function merges the data from the currently active sector with a frozen sector. This is used | |||||
* in the process of reverting back to the previously active sector. | |||||
* The data that is placed in the frozen (the previously active) sector is the node that has been removed from it | |||||
* upon the creation of a new active sector. | |||||
* | |||||
* @param sectorId | |||||
* @private | |||||
*/ | |||||
_mergeThisWithFrozen : function(sectorId) { | |||||
// copy all nodes | |||||
for (var nodeId in this.nodes) { | |||||
if (this.nodes.hasOwnProperty(nodeId)) { | |||||
this.sectors["frozen"][sectorId]["nodes"][nodeId] = this.nodes[nodeId]; | |||||
} | |||||
} | |||||
// copy all edges (if not fully clustered, else there are no edges) | |||||
for (var edgeId in this.edges) { | |||||
if (this.edges.hasOwnProperty(edgeId)) { | |||||
this.sectors["frozen"][sectorId]["edges"][edgeId] = this.edges[edgeId]; | |||||
} | |||||
} | |||||
// merge the nodeIndices | |||||
for (var i = 0; i < this.nodeIndices.length; i++) { | |||||
this.sectors["frozen"][sectorId]["nodeIndices"].push(this.nodeIndices[i]); | |||||
} | |||||
}, | |||||
/** | |||||
* This clusters the sector to one cluster. It was a single cluster before this process started so | |||||
* we revert to that state. The clusterToFit function with a maximum size of 1 node does this. | |||||
* | |||||
* @private | |||||
*/ | |||||
_collapseThisToSingleCluster : function() { | |||||
this.clusterToFit(1,false); | |||||
}, | |||||
/** | |||||
* We create a new active sector from the node that we want to open. | |||||
* | |||||
* @param node | |||||
* @private | |||||
*/ | |||||
_addSector : function(node) { | |||||
// this is the currently active sector | |||||
var sector = this._sector(); | |||||
// // this should allow me to select nodes from a frozen set. | |||||
// if (this.sectors['active'][sector]["nodes"].hasOwnProperty(node.id)) { | |||||
// console.log("the node is part of the active sector"); | |||||
// } | |||||
// else { | |||||
// console.log("I dont know what the fuck happened!!"); | |||||
// } | |||||
// when we switch to a new sector, we remove the node that will be expanded from the current nodes list. | |||||
delete this.nodes[node.id]; | |||||
var unqiueIdentifier = util.randomUUID(); | |||||
// we fully freeze the currently active sector | |||||
this._freezeSector(sector); | |||||
// we create a new active sector. This sector has the Id of the node to ensure uniqueness | |||||
this._createNewSector(unqiueIdentifier); | |||||
// we add the active sector to the sectors array to be able to revert these steps later on | |||||
this._setActiveSector(unqiueIdentifier); | |||||
// we redirect the global references to the new sector's references. this._sector() now returns unqiueIdentifier | |||||
this._switchToSector(this._sector()); | |||||
// finally we add the node we removed from our previous active sector to the new active sector | |||||
this.nodes[node.id] = node; | |||||
}, | |||||
/** | |||||
* We close the sector that is currently open and revert back to the one before. | |||||
* If the active sector is the "default" sector, nothing happens. | |||||
* | |||||
* @private | |||||
*/ | |||||
_collapseSector : function() { | |||||
// the currently active sector | |||||
var sector = this._sector(); | |||||
// we cannot collapse the default sector | |||||
if (sector != "default") { | |||||
if ((this.nodeIndices.length == 1) || | |||||
(this.sectors["active"][sector]["drawingNode"].width*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) || | |||||
(this.sectors["active"][sector]["drawingNode"].height*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) { | |||||
var previousSector = this._previousSector(); | |||||
// we collapse the sector back to a single cluster | |||||
this._collapseThisToSingleCluster(); | |||||
// we move the remaining nodes, edges and nodeIndices to the previous sector. | |||||
// This previous sector is the one we will reactivate | |||||
this._mergeThisWithFrozen(previousSector); | |||||
// the previously active (frozen) sector now has all the data from the currently active sector. | |||||
// we can now delete the active sector. | |||||
this._deleteActiveSector(sector); | |||||
// we activate the previously active (and currently frozen) sector. | |||||
this._activateSector(previousSector); | |||||
// we load the references from the newly active sector into the global references | |||||
this._switchToSector(previousSector); | |||||
// we forget the previously active sector because we reverted to the one before | |||||
this._forgetLastSector(); | |||||
// finally, we update the node index list. | |||||
this._updateNodeIndexList(); | |||||
// we refresh the list with calulation nodes and calculation node indices. | |||||
this._updateCalculationNodes(); | |||||
} | |||||
} | |||||
}, | |||||
/** | |||||
* This runs a function in all active sectors. This is used in _redraw() and the _initializeForceCalculation(). | |||||
* | |||||
* @param {String} runFunction | This is the NAME of a function we want to call in all active sectors | |||||
* | we dont pass the function itself because then the "this" is the window object | |||||
* | instead of the Network object | |||||
* @param {*} [argument] | Optional: arguments to pass to the runFunction | |||||
* @private | |||||
*/ | |||||
_doInAllActiveSectors : function(runFunction,argument) { | |||||
if (argument === undefined) { | |||||
for (var sector in this.sectors["active"]) { | |||||
if (this.sectors["active"].hasOwnProperty(sector)) { | |||||
// switch the global references to those of this sector | |||||
this._switchToActiveSector(sector); | |||||
this[runFunction](); | |||||
} | |||||
} | |||||
} | |||||
else { | |||||
for (var sector in this.sectors["active"]) { | |||||
if (this.sectors["active"].hasOwnProperty(sector)) { | |||||
// switch the global references to those of this sector | |||||
this._switchToActiveSector(sector); | |||||
var args = Array.prototype.splice.call(arguments, 1); | |||||
if (args.length > 1) { | |||||
this[runFunction](args[0],args[1]); | |||||
} | |||||
else { | |||||
this[runFunction](argument); | |||||
} | |||||
} | |||||
} | |||||
} | |||||
// we revert the global references back to our active sector | |||||
this._loadLatestSector(); | |||||
}, | |||||
/** | |||||
* This runs a function in all active sectors. This is used in _redraw() and the _initializeForceCalculation(). | |||||
* | |||||
* @param {String} runFunction | This is the NAME of a function we want to call in all active sectors | |||||
* | we dont pass the function itself because then the "this" is the window object | |||||
* | instead of the Network object | |||||
* @param {*} [argument] | Optional: arguments to pass to the runFunction | |||||
* @private | |||||
*/ | |||||
_doInSupportSector : function(runFunction,argument) { | |||||
if (argument === undefined) { | |||||
this._switchToSupportSector(); | |||||
this[runFunction](); | |||||
} | |||||
else { | |||||
this._switchToSupportSector(); | |||||
var args = Array.prototype.splice.call(arguments, 1); | |||||
if (args.length > 1) { | |||||
this[runFunction](args[0],args[1]); | |||||
} | |||||
else { | |||||
this[runFunction](argument); | |||||
} | |||||
} | |||||
// we revert the global references back to our active sector | |||||
this._loadLatestSector(); | |||||
}, | |||||
/** | |||||
* This runs a function in all frozen sectors. This is used in the _redraw(). | |||||
* | |||||
* @param {String} runFunction | This is the NAME of a function we want to call in all active sectors | |||||
* | we don't pass the function itself because then the "this" is the window object | |||||
* | instead of the Network object | |||||
* @param {*} [argument] | Optional: arguments to pass to the runFunction | |||||
* @private | |||||
*/ | |||||
_doInAllFrozenSectors : function(runFunction,argument) { | |||||
if (argument === undefined) { | |||||
for (var sector in this.sectors["frozen"]) { | |||||
if (this.sectors["frozen"].hasOwnProperty(sector)) { | |||||
// switch the global references to those of this sector | |||||
this._switchToFrozenSector(sector); | |||||
this[runFunction](); | |||||
} | |||||
} | |||||
} | |||||
else { | |||||
for (var sector in this.sectors["frozen"]) { | |||||
if (this.sectors["frozen"].hasOwnProperty(sector)) { | |||||
// switch the global references to those of this sector | |||||
this._switchToFrozenSector(sector); | |||||
var args = Array.prototype.splice.call(arguments, 1); | |||||
if (args.length > 1) { | |||||
this[runFunction](args[0],args[1]); | |||||
} | |||||
else { | |||||
this[runFunction](argument); | |||||
} | |||||
} | |||||
} | |||||
} | |||||
this._loadLatestSector(); | |||||
}, | |||||
/** | |||||
* This runs a function in all sectors. This is used in the _redraw(). | |||||
* | |||||
* @param {String} runFunction | This is the NAME of a function we want to call in all active sectors | |||||
* | we don't pass the function itself because then the "this" is the window object | |||||
* | instead of the Network object | |||||
* @param {*} [argument] | Optional: arguments to pass to the runFunction | |||||
* @private | |||||
*/ | |||||
_doInAllSectors : function(runFunction,argument) { | |||||
var args = Array.prototype.splice.call(arguments, 1); | |||||
if (argument === undefined) { | |||||
this._doInAllActiveSectors(runFunction); | |||||
this._doInAllFrozenSectors(runFunction); | |||||
} | |||||
else { | |||||
if (args.length > 1) { | |||||
this._doInAllActiveSectors(runFunction,args[0],args[1]); | |||||
this._doInAllFrozenSectors(runFunction,args[0],args[1]); | |||||
} | |||||
else { | |||||
this._doInAllActiveSectors(runFunction,argument); | |||||
this._doInAllFrozenSectors(runFunction,argument); | |||||
} | |||||
} | |||||
}, | |||||
/** | |||||
* This clears the nodeIndices list. We cannot use this.nodeIndices = [] because we would break the link with the | |||||
* active sector. Thus we clear the nodeIndices in the active sector, then reconnect the this.nodeIndices to it. | |||||
* | |||||
* @private | |||||
*/ | |||||
_clearNodeIndexList : function() { | |||||
var sector = this._sector(); | |||||
this.sectors["active"][sector]["nodeIndices"] = []; | |||||
this.nodeIndices = this.sectors["active"][sector]["nodeIndices"]; | |||||
}, | |||||
/** | |||||
* Draw the encompassing sector node | |||||
* | |||||
* @param ctx | |||||
* @param sectorType | |||||
* @private | |||||
*/ | |||||
_drawSectorNodes : function(ctx,sectorType) { | |||||
var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node; | |||||
for (var sector in this.sectors[sectorType]) { | |||||
if (this.sectors[sectorType].hasOwnProperty(sector)) { | |||||
if (this.sectors[sectorType][sector]["drawingNode"] !== undefined) { | |||||
this._switchToSector(sector,sectorType); | |||||
minY = 1e9; maxY = -1e9; minX = 1e9; maxX = -1e9; | |||||
for (var nodeId in this.nodes) { | |||||
if (this.nodes.hasOwnProperty(nodeId)) { | |||||
node = this.nodes[nodeId]; | |||||
node.resize(ctx); | |||||
if (minX > node.x - 0.5 * node.width) {minX = node.x - 0.5 * node.width;} | |||||
if (maxX < node.x + 0.5 * node.width) {maxX = node.x + 0.5 * node.width;} | |||||
if (minY > node.y - 0.5 * node.height) {minY = node.y - 0.5 * node.height;} | |||||
if (maxY < node.y + 0.5 * node.height) {maxY = node.y + 0.5 * node.height;} | |||||
} | |||||
} | |||||
node = this.sectors[sectorType][sector]["drawingNode"]; | |||||
node.x = 0.5 * (maxX + minX); | |||||
node.y = 0.5 * (maxY + minY); | |||||
node.width = 2 * (node.x - minX); | |||||
node.height = 2 * (node.y - minY); | |||||
node.radius = Math.sqrt(Math.pow(0.5*node.width,2) + Math.pow(0.5*node.height,2)); | |||||
node.setScale(this.scale); | |||||
node._drawCircle(ctx); | |||||
} | |||||
} | |||||
} | |||||
}, | |||||
_drawAllSectorNodes : function(ctx) { | |||||
this._drawSectorNodes(ctx,"frozen"); | |||||
this._drawSectorNodes(ctx,"active"); | |||||
this._loadLatestSector(); | |||||
} | |||||
}; |