@ -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(); | |||
} | |||
}; |