| @ -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: [ | |||
| './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' | |||
| ], | |||
| dest: VIS_CSS, | |||
| separator: '\n' | |||
| }); | |||
| console.log('created ' + VIS_CSS); | |||
| // concatenate the script files | |||
| concat({ | |||
| dest: VIS_TMP, | |||
| src: [ | |||
| './lib/module/imports.js', | |||
| './lib/shim.js', | |||
| './lib/util.js', | |||
| './lib/DOMutil.js', | |||
| './lib/DataSet.js', | |||
| './lib/DataView.js', | |||
| './lib/timeline/component/GraphGroup.js', | |||
| './lib/timeline/component/Legend.js', | |||
| './lib/timeline/component/DataAxis.js', | |||
| './lib/timeline/component/LineGraph.js', | |||
| './lib/timeline/DataStep.js', | |||
| './lib/timeline/Stack.js', | |||
| './lib/timeline/TimeStep.js', | |||
| './lib/timeline/Range.js', | |||
| './lib/timeline/component/Component.js', | |||
| './lib/timeline/component/TimeAxis.js', | |||
| './lib/timeline/component/CurrentTime.js', | |||
| './lib/timeline/component/CustomTime.js', | |||
| './lib/timeline/component/ItemSet.js', | |||
| './lib/timeline/component/item/*.js', | |||
| './lib/timeline/component/Group.js', | |||
| './lib/timeline/Timeline.js', | |||
| './lib/timeline/Graph2d.js', | |||
| './lib/network/dotparser.js', | |||
| './lib/network/shapes.js', | |||
| './lib/network/Node.js', | |||
| './lib/network/Edge.js', | |||
| './lib/network/Popup.js', | |||
| './lib/network/Groups.js', | |||
| './lib/network/Images.js', | |||
| './lib/network/networkMixins/physics/PhysicsMixin.js', | |||
| './lib/network/networkMixins/physics/HierarchialRepulsion.js', | |||
| './lib/network/networkMixins/physics/BarnesHut.js', | |||
| './lib/network/networkMixins/physics/Repulsion.js', | |||
| './lib/network/networkMixins/HierarchicalLayoutMixin.js', | |||
| './lib/network/networkMixins/ManipulationMixin.js', | |||
| './lib/network/networkMixins/SectorsMixin.js', | |||
| './lib/network/networkMixins/ClusterMixin.js', | |||
| './lib/network/networkMixins/SelectionMixin.js', | |||
| './lib/network/networkMixins/NavigationMixin.js', | |||
| './lib/network/networkMixins/MixinLoader.js', | |||
| './lib/network/Network.js', | |||
| './lib/graph3d/Graph3d.js', | |||
| './lib/module/exports.js' | |||
| ], | |||
| separator: '\n' | |||
| }); | |||
| // copy images | |||
| wrench.copyDirSyncRecursive('./lib/network/img', DIST + '/img/network', { | |||
| forceDelete: true | |||
| }); | |||
| wrench.copyDirSyncRecursive('./lib/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('./lib/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('./lib/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,138 @@ | |||
| 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 | |||
| }); | |||
| 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/**/*.js'], ['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/**/*.js'], ['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; | |||
| @ -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; | |||
| } | |||
| @ -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.'); | |||
| } | |||
| } | |||
| @ -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.'); | |||
| } | |||
| } | |||
| @ -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,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,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(); | |||
| } | |||
| }; | |||
| @ -1,708 +0,0 @@ | |||
| var SelectionMixin = { | |||
| /** | |||
| * This function can be called from the _doInAllSectors function | |||
| * | |||
| * @param object | |||
| * @param overlappingNodes | |||
| * @private | |||
| */ | |||
| _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 | |||
| */ | |||
| _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 | |||
| */ | |||
| _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 | |||
| */ | |||
| _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 | |||
| */ | |||
| _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 | |||
| */ | |||
| _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 | |||
| */ | |||
| _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 | |||
| */ | |||
| _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 | |||
| */ | |||
| _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 | |||
| */ | |||
| _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 | |||
| */ | |||
| _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 | |||
| */ | |||
| _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 | |||
| */ | |||
| _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 | |||
| */ | |||
| _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 | |||
| */ | |||
| _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 | |||
| */ | |||
| _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 | |||
| */ | |||
| _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 | |||
| */ | |||
| _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 | |||
| */ | |||
| _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 | |||
| */ | |||
| _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 | |||
| */ | |||
| _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 | |||
| */ | |||
| _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 | |||
| */ | |||
| _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 | |||
| */ | |||
| _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 | |||
| */ | |||
| _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 | |||
| */ | |||
| _handleTouch : function(pointer) { | |||
| }, | |||
| /** | |||
| * handles the selection part of the tap; | |||
| * | |||
| * @param {Object} pointer | |||
| * @private | |||
| */ | |||
| _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 | |||
| */ | |||
| _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 | |||
| */ | |||
| _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 | |||
| */ | |||
| _handleOnRelease : function(pointer) { | |||
| }, | |||
| /** | |||
| * | |||
| * retrieve the currently selected objects | |||
| * @return {Number[] | String[]} selection An array with the ids of the | |||
| * selected nodes. | |||
| */ | |||
| 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. | |||
| */ | |||
| 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. | |||
| */ | |||
| 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. | |||
| */ | |||
| 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] | |||
| */ | |||
| 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. | |||
| */ | |||
| 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 | |||
| */ | |||
| _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]; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| }; | |||
| @ -1,398 +0,0 @@ | |||
| /** | |||
| * Created by Alex on 2/10/14. | |||
| */ | |||
| var barnesHutMixin = { | |||
| /** | |||
| * 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 | |||
| */ | |||
| _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 | |||
| */ | |||
| _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 | |||
| */ | |||
| _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}, // Center of Mass | |||
| 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 | |||
| */ | |||
| _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 | |||
| */ | |||
| _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 | |||
| */ | |||
| _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 | |||
| */ | |||
| _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 | |||
| */ | |||
| _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 | |||
| */ | |||
| _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 | |||
| */ | |||
| _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(); | |||
| } | |||
| */ | |||
| } | |||
| }; | |||
| @ -1,133 +0,0 @@ | |||
| /** | |||
| * Created by Alex on 2/10/14. | |||
| */ | |||
| var hierarchalRepulsionMixin = { | |||
| /** | |||
| * Calculate the forces the nodes apply on eachother based on a repulsion field. | |||
| * This field is linearly approximated. | |||
| * | |||
| * @private | |||
| */ | |||
| _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 | |||
| */ | |||
| _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; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| }; | |||
| @ -1,706 +0,0 @@ | |||
| /** | |||
| * Created by Alex on 2/6/14. | |||
| */ | |||
| var physicsMixin = { | |||
| /** | |||
| * Toggling barnes Hut calculation on and off. | |||
| * | |||
| * @private | |||
| */ | |||
| _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 | |||
| */ | |||
| _loadSelectedForceSolver: function () { | |||
| // this overloads the this._calculateNodeForces | |||
| if (this.constants.physics.barnesHut.enabled == true) { | |||
| this._clearMixin(repulsionMixin); | |||
| this._clearMixin(hierarchalRepulsionMixin); | |||
| 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(hierarchalRepulsionMixin); | |||
| } | |||
| else { | |||
| this._clearMixin(barnesHutMixin); | |||
| this._clearMixin(hierarchalRepulsionMixin); | |||
| 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 | |||
| */ | |||
| _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 | |||
| */ | |||
| _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 | |||
| */ | |||
| _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 | |||
| */ | |||
| _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 | |||
| */ | |||
| _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 | |||
| */ | |||
| _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 | |||
| */ | |||
| _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 | |||
| */ | |||
| _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 | |||
| */ | |||
| _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(); | |||
| }; | |||
| @ -1,66 +0,0 @@ | |||
| /** | |||
| * Created by Alex on 2/10/14. | |||
| */ | |||
| var repulsionMixin = { | |||
| /** | |||
| * Calculate the forces the nodes apply on eachother based on a repulsion field. | |||
| * This field is linearly approximated. | |||
| * | |||
| * @private | |||
| */ | |||
| _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,252 +0,0 @@ | |||
| // Internet Explorer 8 and older does not support Array.indexOf, so we define | |||
| // it here in that case. | |||
| // http://soledadpenades.com/2007/05/17/arrayindexof-in-internet-explorer/ | |||
| if(!Array.prototype.indexOf) { | |||
| Array.prototype.indexOf = function(obj){ | |||
| for(var i = 0; i < this.length; i++){ | |||
| if(this[i] == obj){ | |||
| return i; | |||
| } | |||
| } | |||
| return -1; | |||
| }; | |||
| try { | |||
| console.log("Warning: Ancient browser detected. Please update your browser"); | |||
| } | |||
| catch (err) { | |||
| } | |||
| } | |||
| // Internet Explorer 8 and older does not support Array.forEach, so we define | |||
| // it here in that case. | |||
| // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/forEach | |||
| if (!Array.prototype.forEach) { | |||
| Array.prototype.forEach = function(fn, scope) { | |||
| for(var i = 0, len = this.length; i < len; ++i) { | |||
| fn.call(scope || this, this[i], i, this); | |||
| } | |||
| } | |||
| } | |||
| // Internet Explorer 8 and older does not support Array.map, so we define it | |||
| // here in that case. | |||
| // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/map | |||
| // Production steps of ECMA-262, Edition 5, 15.4.4.19 | |||
| // Reference: http://es5.github.com/#x15.4.4.19 | |||
| if (!Array.prototype.map) { | |||
| Array.prototype.map = function(callback, thisArg) { | |||
| var T, A, k; | |||
| if (this == null) { | |||
| throw new TypeError(" this is null or not defined"); | |||
| } | |||
| // 1. Let O be the result of calling ToObject passing the |this| value as the argument. | |||
| var O = Object(this); | |||
| // 2. Let lenValue be the result of calling the Get internal method of O with the argument "length". | |||
| // 3. Let len be ToUint32(lenValue). | |||
| var len = O.length >>> 0; | |||
| // 4. If IsCallable(callback) is false, throw a TypeError exception. | |||
| // See: http://es5.github.com/#x9.11 | |||
| if (typeof callback !== "function") { | |||
| throw new TypeError(callback + " is not a function"); | |||
| } | |||
| // 5. If thisArg was supplied, let T be thisArg; else let T be undefined. | |||
| if (thisArg) { | |||
| T = thisArg; | |||
| } | |||
| // 6. Let A be a new array created as if by the expression new Array(len) where Array is | |||
| // the standard built-in constructor with that name and len is the value of len. | |||
| A = new Array(len); | |||
| // 7. Let k be 0 | |||
| k = 0; | |||
| // 8. Repeat, while k < len | |||
| while(k < len) { | |||
| var kValue, mappedValue; | |||
| // a. Let Pk be ToString(k). | |||
| // This is implicit for LHS operands of the in operator | |||
| // b. Let kPresent be the result of calling the HasProperty internal method of O with argument Pk. | |||
| // This step can be combined with c | |||
| // c. If kPresent is true, then | |||
| if (k in O) { | |||
| // i. Let kValue be the result of calling the Get internal method of O with argument Pk. | |||
| kValue = O[ k ]; | |||
| // ii. Let mappedValue be the result of calling the Call internal method of callback | |||
| // with T as the this value and argument list containing kValue, k, and O. | |||
| mappedValue = callback.call(T, kValue, k, O); | |||
| // iii. Call the DefineOwnProperty internal method of A with arguments | |||
| // Pk, Property Descriptor {Value: mappedValue, : true, Enumerable: true, Configurable: true}, | |||
| // and false. | |||
| // In browsers that support Object.defineProperty, use the following: | |||
| // Object.defineProperty(A, Pk, { value: mappedValue, writable: true, enumerable: true, configurable: true }); | |||
| // For best browser support, use the following: | |||
| A[ k ] = mappedValue; | |||
| } | |||
| // d. Increase k by 1. | |||
| k++; | |||
| } | |||
| // 9. return A | |||
| return A; | |||
| }; | |||
| } | |||
| // Internet Explorer 8 and older does not support Array.filter, so we define it | |||
| // here in that case. | |||
| // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/filter | |||
| if (!Array.prototype.filter) { | |||
| Array.prototype.filter = function(fun /*, thisp */) { | |||
| "use strict"; | |||
| if (this == null) { | |||
| throw new TypeError(); | |||
| } | |||
| var t = Object(this); | |||
| var len = t.length >>> 0; | |||
| if (typeof fun != "function") { | |||
| throw new TypeError(); | |||
| } | |||
| var res = []; | |||
| var thisp = arguments[1]; | |||
| for (var i = 0; i < len; i++) { | |||
| if (i in t) { | |||
| var val = t[i]; // in case fun mutates this | |||
| if (fun.call(thisp, val, i, t)) | |||
| res.push(val); | |||
| } | |||
| } | |||
| return res; | |||
| }; | |||
| } | |||
| // Internet Explorer 8 and older does not support Object.keys, so we define it | |||
| // here in that case. | |||
| // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/keys | |||
| if (!Object.keys) { | |||
| Object.keys = (function () { | |||
| var hasOwnProperty = Object.prototype.hasOwnProperty, | |||
| hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'), | |||
| dontEnums = [ | |||
| 'toString', | |||
| 'toLocaleString', | |||
| 'valueOf', | |||
| 'hasOwnProperty', | |||
| 'isPrototypeOf', | |||
| 'propertyIsEnumerable', | |||
| 'constructor' | |||
| ], | |||
| dontEnumsLength = dontEnums.length; | |||
| return function (obj) { | |||
| if (typeof obj !== 'object' && typeof obj !== 'function' || obj === null) { | |||
| throw new TypeError('Object.keys called on non-object'); | |||
| } | |||
| var result = []; | |||
| for (var prop in obj) { | |||
| if (hasOwnProperty.call(obj, prop)) result.push(prop); | |||
| } | |||
| if (hasDontEnumBug) { | |||
| for (var i=0; i < dontEnumsLength; i++) { | |||
| if (hasOwnProperty.call(obj, dontEnums[i])) result.push(dontEnums[i]); | |||
| } | |||
| } | |||
| return result; | |||
| } | |||
| })() | |||
| } | |||
| // Internet Explorer 8 and older does not support Array.isArray, | |||
| // so we define it here in that case. | |||
| // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/isArray | |||
| if(!Array.isArray) { | |||
| Array.isArray = function (vArg) { | |||
| return Object.prototype.toString.call(vArg) === "[object Array]"; | |||
| }; | |||
| } | |||
| // Internet Explorer 8 and older does not support Function.bind, | |||
| // so we define it here in that case. | |||
| // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Function/bind | |||
| if (!Function.prototype.bind) { | |||
| Function.prototype.bind = function (oThis) { | |||
| if (typeof this !== "function") { | |||
| // closest thing possible to the ECMAScript 5 internal IsCallable function | |||
| throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable"); | |||
| } | |||
| var aArgs = Array.prototype.slice.call(arguments, 1), | |||
| fToBind = this, | |||
| fNOP = function () {}, | |||
| fBound = function () { | |||
| return fToBind.apply(this instanceof fNOP && oThis | |||
| ? this | |||
| : oThis, | |||
| aArgs.concat(Array.prototype.slice.call(arguments))); | |||
| }; | |||
| fNOP.prototype = this.prototype; | |||
| fBound.prototype = new fNOP(); | |||
| return fBound; | |||
| }; | |||
| } | |||
| // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/create | |||
| if (!Object.create) { | |||
| Object.create = function (o) { | |||
| if (arguments.length > 1) { | |||
| throw new Error('Object.create implementation only accepts the first parameter.'); | |||
| } | |||
| function F() {} | |||
| F.prototype = o; | |||
| return new F(); | |||
| }; | |||
| } | |||
| // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind | |||
| if (!Function.prototype.bind) { | |||
| Function.prototype.bind = function (oThis) { | |||
| if (typeof this !== "function") { | |||
| // closest thing possible to the ECMAScript 5 internal IsCallable function | |||
| throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable"); | |||
| } | |||
| var aArgs = Array.prototype.slice.call(arguments, 1), | |||
| fToBind = this, | |||
| fNOP = function () {}, | |||
| fBound = function () { | |||
| return fToBind.apply(this instanceof fNOP && oThis | |||
| ? this | |||
| : oThis, | |||
| aArgs.concat(Array.prototype.slice.call(arguments))); | |||
| }; | |||
| fNOP.prototype = this.prototype; | |||
| fBound.prototype = new fNOP(); | |||
| return fBound; | |||
| }; | |||
| } | |||