| @ -1,208 +0,0 @@ | |||||
| /** | |||||
| * Jake build script | |||||
| */ | |||||
| var jake = require('jake'), | |||||
| browserify = require('browserify'), | |||||
| wrench = require('wrench'), | |||||
| CleanCSS = require('clean-css'), | |||||
| fs = require('fs'); | |||||
| require('jake-utils'); | |||||
| // constants | |||||
| var DIST = './dist'; | |||||
| var VIS = DIST + '/vis.js'; | |||||
| var VIS_CSS = DIST + '/vis.css'; | |||||
| var VIS_TMP = DIST + '/vis.js.tmp'; | |||||
| var VIS_MIN = DIST + '/vis.min.js'; | |||||
| var VIS_MIN_CSS = DIST + '/vis.min.css'; | |||||
| /** | |||||
| * default task | |||||
| */ | |||||
| desc('Default task: build all libraries'); | |||||
| task('default', ['build', 'minify'], function () { | |||||
| console.log('done'); | |||||
| }); | |||||
| /** | |||||
| * build the visualization library vis.js | |||||
| */ | |||||
| desc('Build the visualization library vis.js'); | |||||
| task('build', {async: true}, function () { | |||||
| jake.mkdirP(DIST); | |||||
| jake.mkdirP(DIST + '/img'); | |||||
| // concatenate and stringify the css files | |||||
| concat({ | |||||
| src: [ | |||||
| './src/timeline/component/css/timeline.css', | |||||
| './src/timeline/component/css/panel.css', | |||||
| './src/timeline/component/css/labelset.css', | |||||
| './src/timeline/component/css/itemset.css', | |||||
| './src/timeline/component/css/item.css', | |||||
| './src/timeline/component/css/timeaxis.css', | |||||
| './src/timeline/component/css/currenttime.css', | |||||
| './src/timeline/component/css/customtime.css', | |||||
| './src/timeline/component/css/animation.css', | |||||
| './src/timeline/component/css/dataaxis.css', | |||||
| './src/timeline/component/css/pathStyles.css', | |||||
| './src/network/css/network-manipulation.css', | |||||
| './src/network/css/network-navigation.css' | |||||
| ], | |||||
| dest: VIS_CSS, | |||||
| separator: '\n' | |||||
| }); | |||||
| console.log('created ' + VIS_CSS); | |||||
| // concatenate the script files | |||||
| concat({ | |||||
| dest: VIS_TMP, | |||||
| src: [ | |||||
| './src/module/imports.js', | |||||
| './src/shim.js', | |||||
| './src/util.js', | |||||
| './src/DOMutil.js', | |||||
| './src/DataSet.js', | |||||
| './src/DataView.js', | |||||
| './src/timeline/component/GraphGroup.js', | |||||
| './src/timeline/component/Legend.js', | |||||
| './src/timeline/component/DataAxis.js', | |||||
| './src/timeline/component/LineGraph.js', | |||||
| './src/timeline/DataStep.js', | |||||
| './src/timeline/Stack.js', | |||||
| './src/timeline/TimeStep.js', | |||||
| './src/timeline/Range.js', | |||||
| './src/timeline/component/Component.js', | |||||
| './src/timeline/component/TimeAxis.js', | |||||
| './src/timeline/component/CurrentTime.js', | |||||
| './src/timeline/component/CustomTime.js', | |||||
| './src/timeline/component/ItemSet.js', | |||||
| './src/timeline/component/item/*.js', | |||||
| './src/timeline/component/Group.js', | |||||
| './src/timeline/Timeline.js', | |||||
| './src/timeline/Graph2d.js', | |||||
| './src/network/dotparser.js', | |||||
| './src/network/shapes.js', | |||||
| './src/network/Node.js', | |||||
| './src/network/Edge.js', | |||||
| './src/network/Popup.js', | |||||
| './src/network/Groups.js', | |||||
| './src/network/Images.js', | |||||
| './src/network/networkMixins/physics/PhysicsMixin.js', | |||||
| './src/network/networkMixins/physics/HierarchialRepulsion.js', | |||||
| './src/network/networkMixins/physics/BarnesHut.js', | |||||
| './src/network/networkMixins/physics/Repulsion.js', | |||||
| './src/network/networkMixins/HierarchicalLayoutMixin.js', | |||||
| './src/network/networkMixins/ManipulationMixin.js', | |||||
| './src/network/networkMixins/SectorsMixin.js', | |||||
| './src/network/networkMixins/ClusterMixin.js', | |||||
| './src/network/networkMixins/SelectionMixin.js', | |||||
| './src/network/networkMixins/NavigationMixin.js', | |||||
| './src/network/networkMixins/MixinLoader.js', | |||||
| './src/network/Network.js', | |||||
| './src/graph3d/Graph3d.js', | |||||
| './src/module/exports.js' | |||||
| ], | |||||
| separator: '\n' | |||||
| }); | |||||
| // copy images | |||||
| wrench.copyDirSyncRecursive('./src/network/img', DIST + '/img/network', { | |||||
| forceDelete: true | |||||
| }); | |||||
| wrench.copyDirSyncRecursive('./src/timeline/img', DIST + '/img/timeline', { | |||||
| forceDelete: true | |||||
| }); | |||||
| var timeStart = Date.now(); | |||||
| // bundle the concatenated script and dependencies into one file | |||||
| var b = browserify(); | |||||
| b.add(VIS_TMP); | |||||
| b.bundle({ | |||||
| standalone: 'vis' | |||||
| }, function (err, code) { | |||||
| if(err) { | |||||
| throw err; | |||||
| } | |||||
| console.log("browserify",Date.now() - timeStart); timeStart = Date.now(); | |||||
| // add header and footer | |||||
| var lib = read('./src/module/header.js') + code; | |||||
| // write bundled file | |||||
| write(VIS, lib); | |||||
| console.log('created js' + VIS); | |||||
| // remove temporary file | |||||
| fs.unlinkSync(VIS_TMP); | |||||
| // update version number and stuff in the javascript files | |||||
| replacePlaceholders(VIS); | |||||
| complete(); | |||||
| }); | |||||
| }); | |||||
| /** | |||||
| * minify the visualization library vis.js | |||||
| */ | |||||
| desc('Minify the visualization library vis.js'); | |||||
| task('minify', {async: true}, function () { | |||||
| // minify javascript | |||||
| minify({ | |||||
| src: VIS, | |||||
| dest: VIS_MIN, | |||||
| header: read('./src/module/header.js') | |||||
| }); | |||||
| // update version number and stuff in the javascript files | |||||
| replacePlaceholders(VIS_MIN); | |||||
| console.log('created minified ' + VIS_MIN); | |||||
| var minified = new CleanCSS().minify(read(VIS_CSS)); | |||||
| write(VIS_MIN_CSS, minified); | |||||
| console.log('created minified ' + VIS_MIN_CSS); | |||||
| }); | |||||
| /** | |||||
| * test task | |||||
| */ | |||||
| desc('Test the library'); | |||||
| task('test', function () { | |||||
| // TODO: use a testing suite for testing: nodeunit, mocha, tap, ... | |||||
| var filelist = new jake.FileList(); | |||||
| filelist.include([ | |||||
| './test/**/*.js' | |||||
| ]); | |||||
| var files = filelist.toArray(); | |||||
| files.forEach(function (file) { | |||||
| require('./' + file); | |||||
| }); | |||||
| console.log('Executed ' + files.length + ' test files successfully'); | |||||
| }); | |||||
| /** | |||||
| * replace version, date, and name placeholders in the provided file | |||||
| * @param {String} filename | |||||
| */ | |||||
| var replacePlaceholders = function (filename) { | |||||
| replace({ | |||||
| replacements: [ | |||||
| {pattern: '@@date', replacement: today()}, | |||||
| {pattern: '@@version', replacement: version()} | |||||
| ], | |||||
| src: filename | |||||
| }); | |||||
| }; | |||||
| @ -0,0 +1,139 @@ | |||||
| var fs = require('fs'); | |||||
| var gulp = require('gulp'); | |||||
| var gutil = require('gulp-util'); | |||||
| var concat = require('gulp-concat'); | |||||
| var minifyCSS = require('gulp-minify-css'); | |||||
| var rename = require("gulp-rename"); | |||||
| var webpack = require('webpack'); | |||||
| var uglify = require('uglify-js'); | |||||
| var rimraf = require('rimraf'); | |||||
| var merge = require('merge-stream'); | |||||
| var ENTRY = './index.js'; | |||||
| var HEADER = './lib/header.js'; | |||||
| var DIST = './dist'; | |||||
| var VIS_JS = 'vis.js'; | |||||
| var VIS_MAP = 'vis.map'; | |||||
| var VIS_CSS = 'vis.css'; | |||||
| var VIS_MIN_CSS = 'vis.min.css'; | |||||
| var DIST_VIS_MIN_JS = DIST + '/vis.min.js'; | |||||
| var DIST_VIS_MAP = DIST + '/' + VIS_MAP; | |||||
| // generate banner with today's date and correct version | |||||
| function createBanner() { | |||||
| var today = gutil.date(new Date(), 'yyyy-mm-dd'); // today, formatted as yyyy-mm-dd | |||||
| var version = require('./package.json').version; | |||||
| return String(fs.readFileSync(HEADER)) | |||||
| .replace('@@date', today) | |||||
| .replace('@@version', version); | |||||
| } | |||||
| var bannerPlugin = new webpack.BannerPlugin(createBanner(), { | |||||
| entryOnly: true, | |||||
| raw: true | |||||
| }); | |||||
| // TODO: the moment.js language files should be excluded by default (they are quite big) | |||||
| var webpackConfig = { | |||||
| entry: ENTRY, | |||||
| output: { | |||||
| library: 'vis', | |||||
| libraryTarget: 'umd', | |||||
| path: DIST, | |||||
| filename: VIS_JS, | |||||
| sourcePrefix: ' ' | |||||
| }, | |||||
| plugins: [ bannerPlugin ], | |||||
| cache: true | |||||
| }; | |||||
| var uglifyConfig = { | |||||
| outSourceMap: VIS_MAP, | |||||
| output: { | |||||
| comments: /@license/ | |||||
| } | |||||
| }; | |||||
| // create a single instance of the compiler to allow caching | |||||
| var compiler = webpack(webpackConfig); | |||||
| // clean the dist directory | |||||
| gulp.task('clean', function (cb) { | |||||
| rimraf(DIST, cb); | |||||
| }); | |||||
| gulp.task('bundle-js', ['clean'], function (cb) { | |||||
| // update the banner contents (has a date in it which should stay up to date) | |||||
| bannerPlugin.banner = createBanner(); | |||||
| compiler.run(function (err, stats) { | |||||
| if (err) gutil.log(err); | |||||
| cb(); | |||||
| }); | |||||
| }); | |||||
| // bundle and minify css | |||||
| gulp.task('bundle-css', ['clean'], function () { | |||||
| var files = [ | |||||
| './lib/timeline/component/css/timeline.css', | |||||
| './lib/timeline/component/css/panel.css', | |||||
| './lib/timeline/component/css/labelset.css', | |||||
| './lib/timeline/component/css/itemset.css', | |||||
| './lib/timeline/component/css/item.css', | |||||
| './lib/timeline/component/css/timeaxis.css', | |||||
| './lib/timeline/component/css/currenttime.css', | |||||
| './lib/timeline/component/css/customtime.css', | |||||
| './lib/timeline/component/css/animation.css', | |||||
| './lib/timeline/component/css/dataaxis.css', | |||||
| './lib/timeline/component/css/pathStyles.css', | |||||
| './lib/network/css/network-manipulation.css', | |||||
| './lib/network/css/network-navigation.css' | |||||
| ]; | |||||
| return gulp.src(files) | |||||
| .pipe(concat(VIS_CSS)) | |||||
| .pipe(gulp.dest(DIST)) | |||||
| // TODO: nicer to put minifying css in a separate task? | |||||
| .pipe(minifyCSS()) | |||||
| .pipe(rename(VIS_MIN_CSS)) | |||||
| .pipe(gulp.dest(DIST)); | |||||
| }); | |||||
| gulp.task('copy-img', ['clean'], function () { | |||||
| var network = gulp.src('./lib/network/img/**/*') | |||||
| .pipe(gulp.dest(DIST + '/img/network')); | |||||
| var timeline = gulp.src('./lib/timeline/img/**/*') | |||||
| .pipe(gulp.dest(DIST + '/img/timeline')); | |||||
| return merge(network, timeline); | |||||
| }); | |||||
| gulp.task('minify', ['bundle-js'], function (cb) { | |||||
| var result = uglify.minify([DIST + '/' + VIS_JS], uglifyConfig); | |||||
| fs.writeFileSync(DIST_VIS_MIN_JS, result.code); | |||||
| fs.writeFileSync(DIST_VIS_MAP, result.map); | |||||
| cb(); | |||||
| }); | |||||
| gulp.task('bundle', ['bundle-js', 'bundle-css', 'copy-img']); | |||||
| // The watch task (to automatically rebuild when the source code changes) | |||||
| gulp.task('watch', ['bundle', 'minify'], function () { | |||||
| gulp.watch(['index.js', 'lib/**/*'], ['bundle', 'minify']); | |||||
| }); | |||||
| // The watch task (to automatically rebuild when the source code changes) | |||||
| // this watch only rebuilds vis.js, not vis.min.js | |||||
| gulp.task('watch-dev', ['bundle'], function () { | |||||
| gulp.watch(['index.js', 'lib/**/*'], ['bundle']); | |||||
| }); | |||||
| // The default task (called when you run `gulp`) | |||||
| gulp.task('default', ['clean', 'bundle', 'minify']); | |||||
| @ -0,0 +1,56 @@ | |||||
| // utils | |||||
| exports.util = require('./lib/util'); | |||||
| exports.DOMutil = require('./lib/DOMutil'); | |||||
| // data | |||||
| exports.DataSet = require('./lib/DataSet'); | |||||
| exports.DataView = require('./lib/DataView'); | |||||
| // Graph3d | |||||
| exports.Graph3d = require('./lib/graph3d/Graph3d'); | |||||
| // Timeline | |||||
| exports.Timeline = require('./lib/timeline/Timeline'); | |||||
| exports.Graph2d = require('./lib/timeline/Graph2d'); | |||||
| exports.timeline= { | |||||
| DataStep: require('./lib/timeline/DataStep'), | |||||
| Range: require('./lib/timeline/Range'), | |||||
| stack: require('./lib/timeline/Stack'), | |||||
| TimeStep: require('./lib/timeline/TimeStep'), | |||||
| components: { | |||||
| items: { | |||||
| Item: require('./lib/timeline/component/item/Item'), | |||||
| ItemBox: require('./lib/timeline/component/item/ItemBox'), | |||||
| ItemPoint: require('./lib/timeline/component/item/ItemPoint'), | |||||
| ItemRange: require('./lib/timeline/component/item/ItemRange') | |||||
| }, | |||||
| Component: require('./lib/timeline/component/Component'), | |||||
| CurrentTime: require('./lib/timeline/component/CurrentTime'), | |||||
| CustomTime: require('./lib/timeline/component/CustomTime'), | |||||
| DataAxis: require('./lib/timeline/component/DataAxis'), | |||||
| GraphGroup: require('./lib/timeline/component/GraphGroup'), | |||||
| Group: require('./lib/timeline/component/Group'), | |||||
| ItemSet: require('./lib/timeline/component/ItemSet'), | |||||
| Legend: require('./lib/timeline/component/Legend'), | |||||
| LineGraph: require('./lib/timeline/component/LineGraph'), | |||||
| TimeAxis: require('./lib/timeline/component/TimeAxis') | |||||
| } | |||||
| }; | |||||
| // Network | |||||
| exports.Network = require('./lib/network/Network'); | |||||
| exports.network = { | |||||
| Edge: require('./lib/network/Edge'), | |||||
| Groups: require('./lib/network/Groups'), | |||||
| Images: require('./lib/network/Images'), | |||||
| Node: require('./lib/network/Node'), | |||||
| Popup: require('./lib/network/Popup'), | |||||
| dotparser: require('./lib/network/dotparser') | |||||
| }; | |||||
| // Deprecated since v3.0.0 | |||||
| exports.Graph = function () { | |||||
| throw new Error('Graph is renamed to Network. Please create a graph as new vis.Network(...)'); | |||||
| }; | |||||
| @ -0,0 +1,218 @@ | |||||
| var DataView = require('../DataView'); | |||||
| /** | |||||
| * @class Filter | |||||
| * | |||||
| * @param {DataSet} data The google data table | |||||
| * @param {Number} column The index of the column to be filtered | |||||
| * @param {Graph} graph The graph | |||||
| */ | |||||
| function Filter (data, column, graph) { | |||||
| this.data = data; | |||||
| this.column = column; | |||||
| this.graph = graph; // the parent graph | |||||
| this.index = undefined; | |||||
| this.value = undefined; | |||||
| // read all distinct values and select the first one | |||||
| this.values = graph.getDistinctValues(data.get(), this.column); | |||||
| // sort both numeric and string values correctly | |||||
| this.values.sort(function (a, b) { | |||||
| return a > b ? 1 : a < b ? -1 : 0; | |||||
| }); | |||||
| if (this.values.length > 0) { | |||||
| this.selectValue(0); | |||||
| } | |||||
| // create an array with the filtered datapoints. this will be loaded afterwards | |||||
| this.dataPoints = []; | |||||
| this.loaded = false; | |||||
| this.onLoadCallback = undefined; | |||||
| if (graph.animationPreload) { | |||||
| this.loaded = false; | |||||
| this.loadInBackground(); | |||||
| } | |||||
| else { | |||||
| this.loaded = true; | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Return the label | |||||
| * @return {string} label | |||||
| */ | |||||
| Filter.prototype.isLoaded = function() { | |||||
| return this.loaded; | |||||
| }; | |||||
| /** | |||||
| * Return the loaded progress | |||||
| * @return {Number} percentage between 0 and 100 | |||||
| */ | |||||
| Filter.prototype.getLoadedProgress = function() { | |||||
| var len = this.values.length; | |||||
| var i = 0; | |||||
| while (this.dataPoints[i]) { | |||||
| i++; | |||||
| } | |||||
| return Math.round(i / len * 100); | |||||
| }; | |||||
| /** | |||||
| * Return the label | |||||
| * @return {string} label | |||||
| */ | |||||
| Filter.prototype.getLabel = function() { | |||||
| return this.graph.filterLabel; | |||||
| }; | |||||
| /** | |||||
| * Return the columnIndex of the filter | |||||
| * @return {Number} columnIndex | |||||
| */ | |||||
| Filter.prototype.getColumn = function() { | |||||
| return this.column; | |||||
| }; | |||||
| /** | |||||
| * Return the currently selected value. Returns undefined if there is no selection | |||||
| * @return {*} value | |||||
| */ | |||||
| Filter.prototype.getSelectedValue = function() { | |||||
| if (this.index === undefined) | |||||
| return undefined; | |||||
| return this.values[this.index]; | |||||
| }; | |||||
| /** | |||||
| * Retrieve all values of the filter | |||||
| * @return {Array} values | |||||
| */ | |||||
| Filter.prototype.getValues = function() { | |||||
| return this.values; | |||||
| }; | |||||
| /** | |||||
| * Retrieve one value of the filter | |||||
| * @param {Number} index | |||||
| * @return {*} value | |||||
| */ | |||||
| Filter.prototype.getValue = function(index) { | |||||
| if (index >= this.values.length) | |||||
| throw 'Error: index out of range'; | |||||
| return this.values[index]; | |||||
| }; | |||||
| /** | |||||
| * Retrieve the (filtered) dataPoints for the currently selected filter index | |||||
| * @param {Number} [index] (optional) | |||||
| * @return {Array} dataPoints | |||||
| */ | |||||
| Filter.prototype._getDataPoints = function(index) { | |||||
| if (index === undefined) | |||||
| index = this.index; | |||||
| if (index === undefined) | |||||
| return []; | |||||
| var dataPoints; | |||||
| if (this.dataPoints[index]) { | |||||
| dataPoints = this.dataPoints[index]; | |||||
| } | |||||
| else { | |||||
| var f = {}; | |||||
| f.column = this.column; | |||||
| f.value = this.values[index]; | |||||
| var dataView = new DataView(this.data,{filter: function (item) {return (item[f.column] == f.value);}}).get(); | |||||
| dataPoints = this.graph._getDataPoints(dataView); | |||||
| this.dataPoints[index] = dataPoints; | |||||
| } | |||||
| return dataPoints; | |||||
| }; | |||||
| /** | |||||
| * Set a callback function when the filter is fully loaded. | |||||
| */ | |||||
| Filter.prototype.setOnLoadCallback = function(callback) { | |||||
| this.onLoadCallback = callback; | |||||
| }; | |||||
| /** | |||||
| * Add a value to the list with available values for this filter | |||||
| * No double entries will be created. | |||||
| * @param {Number} index | |||||
| */ | |||||
| Filter.prototype.selectValue = function(index) { | |||||
| if (index >= this.values.length) | |||||
| throw 'Error: index out of range'; | |||||
| this.index = index; | |||||
| this.value = this.values[index]; | |||||
| }; | |||||
| /** | |||||
| * Load all filtered rows in the background one by one | |||||
| * Start this method without providing an index! | |||||
| */ | |||||
| Filter.prototype.loadInBackground = function(index) { | |||||
| if (index === undefined) | |||||
| index = 0; | |||||
| var frame = this.graph.frame; | |||||
| if (index < this.values.length) { | |||||
| var dataPointsTemp = this._getDataPoints(index); | |||||
| //this.graph.redrawInfo(); // TODO: not neat | |||||
| // create a progress box | |||||
| if (frame.progress === undefined) { | |||||
| frame.progress = document.createElement('DIV'); | |||||
| frame.progress.style.position = 'absolute'; | |||||
| frame.progress.style.color = 'gray'; | |||||
| frame.appendChild(frame.progress); | |||||
| } | |||||
| var progress = this.getLoadedProgress(); | |||||
| frame.progress.innerHTML = 'Loading animation... ' + progress + '%'; | |||||
| // TODO: this is no nice solution... | |||||
| frame.progress.style.bottom = 60 + 'px'; // TODO: use height of slider | |||||
| frame.progress.style.left = 10 + 'px'; | |||||
| var me = this; | |||||
| setTimeout(function() {me.loadInBackground(index+1);}, 10); | |||||
| this.loaded = false; | |||||
| } | |||||
| else { | |||||
| this.loaded = true; | |||||
| // remove the progress box | |||||
| if (frame.progress !== undefined) { | |||||
| frame.removeChild(frame.progress); | |||||
| frame.progress = undefined; | |||||
| } | |||||
| if (this.onLoadCallback) | |||||
| this.onLoadCallback(); | |||||
| } | |||||
| }; | |||||
| module.exports = Filter; | |||||
| @ -0,0 +1,11 @@ | |||||
| /** | |||||
| * @prototype Point2d | |||||
| * @param {Number} [x] | |||||
| * @param {Number} [y] | |||||
| */ | |||||
| Point2d = function (x, y) { | |||||
| this.x = x !== undefined ? x : 0; | |||||
| this.y = y !== undefined ? y : 0; | |||||
| }; | |||||
| module.exports = Point2d; | |||||
| @ -0,0 +1,85 @@ | |||||
| /** | |||||
| * @prototype Point3d | |||||
| * @param {Number} [x] | |||||
| * @param {Number} [y] | |||||
| * @param {Number} [z] | |||||
| */ | |||||
| function Point3d(x, y, z) { | |||||
| this.x = x !== undefined ? x : 0; | |||||
| this.y = y !== undefined ? y : 0; | |||||
| this.z = z !== undefined ? z : 0; | |||||
| }; | |||||
| /** | |||||
| * Subtract the two provided points, returns a-b | |||||
| * @param {Point3d} a | |||||
| * @param {Point3d} b | |||||
| * @return {Point3d} a-b | |||||
| */ | |||||
| Point3d.subtract = function(a, b) { | |||||
| var sub = new Point3d(); | |||||
| sub.x = a.x - b.x; | |||||
| sub.y = a.y - b.y; | |||||
| sub.z = a.z - b.z; | |||||
| return sub; | |||||
| }; | |||||
| /** | |||||
| * Add the two provided points, returns a+b | |||||
| * @param {Point3d} a | |||||
| * @param {Point3d} b | |||||
| * @return {Point3d} a+b | |||||
| */ | |||||
| Point3d.add = function(a, b) { | |||||
| var sum = new Point3d(); | |||||
| sum.x = a.x + b.x; | |||||
| sum.y = a.y + b.y; | |||||
| sum.z = a.z + b.z; | |||||
| return sum; | |||||
| }; | |||||
| /** | |||||
| * Calculate the average of two 3d points | |||||
| * @param {Point3d} a | |||||
| * @param {Point3d} b | |||||
| * @return {Point3d} The average, (a+b)/2 | |||||
| */ | |||||
| Point3d.avg = function(a, b) { | |||||
| return new Point3d( | |||||
| (a.x + b.x) / 2, | |||||
| (a.y + b.y) / 2, | |||||
| (a.z + b.z) / 2 | |||||
| ); | |||||
| }; | |||||
| /** | |||||
| * Calculate the cross product of the two provided points, returns axb | |||||
| * Documentation: http://en.wikipedia.org/wiki/Cross_product | |||||
| * @param {Point3d} a | |||||
| * @param {Point3d} b | |||||
| * @return {Point3d} cross product axb | |||||
| */ | |||||
| Point3d.crossProduct = function(a, b) { | |||||
| var crossproduct = new Point3d(); | |||||
| crossproduct.x = a.y * b.z - a.z * b.y; | |||||
| crossproduct.y = a.z * b.x - a.x * b.z; | |||||
| crossproduct.z = a.x * b.y - a.y * b.x; | |||||
| return crossproduct; | |||||
| }; | |||||
| /** | |||||
| * Rtrieve the length of the vector (or the distance from this point to the origin | |||||
| * @return {Number} length | |||||
| */ | |||||
| Point3d.prototype.length = function() { | |||||
| return Math.sqrt( | |||||
| this.x * this.x + | |||||
| this.y * this.y + | |||||
| this.z * this.z | |||||
| ); | |||||
| }; | |||||
| module.exports = Point3d; | |||||
| @ -0,0 +1,140 @@ | |||||
| /** | |||||
| * @prototype StepNumber | |||||
| * The class StepNumber is an iterator for Numbers. You provide a start and end | |||||
| * value, and a best step size. StepNumber itself rounds to fixed values and | |||||
| * a finds the step that best fits the provided step. | |||||
| * | |||||
| * If prettyStep is true, the step size is chosen as close as possible to the | |||||
| * provided step, but being a round value like 1, 2, 5, 10, 20, 50, .... | |||||
| * | |||||
| * Example usage: | |||||
| * var step = new StepNumber(0, 10, 2.5, true); | |||||
| * step.start(); | |||||
| * while (!step.end()) { | |||||
| * alert(step.getCurrent()); | |||||
| * step.next(); | |||||
| * } | |||||
| * | |||||
| * Version: 1.0 | |||||
| * | |||||
| * @param {Number} start The start value | |||||
| * @param {Number} end The end value | |||||
| * @param {Number} step Optional. Step size. Must be a positive value. | |||||
| * @param {boolean} prettyStep Optional. If true, the step size is rounded | |||||
| * To a pretty step size (like 1, 2, 5, 10, 20, 50, ...) | |||||
| */ | |||||
| function StepNumber(start, end, step, prettyStep) { | |||||
| // set default values | |||||
| this._start = 0; | |||||
| this._end = 0; | |||||
| this._step = 1; | |||||
| this.prettyStep = true; | |||||
| this.precision = 5; | |||||
| this._current = 0; | |||||
| this.setRange(start, end, step, prettyStep); | |||||
| }; | |||||
| /** | |||||
| * Set a new range: start, end and step. | |||||
| * | |||||
| * @param {Number} start The start value | |||||
| * @param {Number} end The end value | |||||
| * @param {Number} step Optional. Step size. Must be a positive value. | |||||
| * @param {boolean} prettyStep Optional. If true, the step size is rounded | |||||
| * To a pretty step size (like 1, 2, 5, 10, 20, 50, ...) | |||||
| */ | |||||
| StepNumber.prototype.setRange = function(start, end, step, prettyStep) { | |||||
| this._start = start ? start : 0; | |||||
| this._end = end ? end : 0; | |||||
| this.setStep(step, prettyStep); | |||||
| }; | |||||
| /** | |||||
| * Set a new step size | |||||
| * @param {Number} step New step size. Must be a positive value | |||||
| * @param {boolean} prettyStep Optional. If true, the provided step is rounded | |||||
| * to a pretty step size (like 1, 2, 5, 10, 20, 50, ...) | |||||
| */ | |||||
| StepNumber.prototype.setStep = function(step, prettyStep) { | |||||
| if (step === undefined || step <= 0) | |||||
| return; | |||||
| if (prettyStep !== undefined) | |||||
| this.prettyStep = prettyStep; | |||||
| if (this.prettyStep === true) | |||||
| this._step = StepNumber.calculatePrettyStep(step); | |||||
| else | |||||
| this._step = step; | |||||
| }; | |||||
| /** | |||||
| * Calculate a nice step size, closest to the desired step size. | |||||
| * Returns a value in one of the ranges 1*10^n, 2*10^n, or 5*10^n, where n is an | |||||
| * integer Number. For example 1, 2, 5, 10, 20, 50, etc... | |||||
| * @param {Number} step Desired step size | |||||
| * @return {Number} Nice step size | |||||
| */ | |||||
| StepNumber.calculatePrettyStep = function (step) { | |||||
| var log10 = function (x) {return Math.log(x) / Math.LN10;}; | |||||
| // try three steps (multiple of 1, 2, or 5 | |||||
| var step1 = Math.pow(10, Math.round(log10(step))), | |||||
| step2 = 2 * Math.pow(10, Math.round(log10(step / 2))), | |||||
| step5 = 5 * Math.pow(10, Math.round(log10(step / 5))); | |||||
| // choose the best step (closest to minimum step) | |||||
| var prettyStep = step1; | |||||
| if (Math.abs(step2 - step) <= Math.abs(prettyStep - step)) prettyStep = step2; | |||||
| if (Math.abs(step5 - step) <= Math.abs(prettyStep - step)) prettyStep = step5; | |||||
| // for safety | |||||
| if (prettyStep <= 0) { | |||||
| prettyStep = 1; | |||||
| } | |||||
| return prettyStep; | |||||
| }; | |||||
| /** | |||||
| * returns the current value of the step | |||||
| * @return {Number} current value | |||||
| */ | |||||
| StepNumber.prototype.getCurrent = function () { | |||||
| return parseFloat(this._current.toPrecision(this.precision)); | |||||
| }; | |||||
| /** | |||||
| * returns the current step size | |||||
| * @return {Number} current step size | |||||
| */ | |||||
| StepNumber.prototype.getStep = function () { | |||||
| return this._step; | |||||
| }; | |||||
| /** | |||||
| * Set the current value to the largest value smaller than start, which | |||||
| * is a multiple of the step size | |||||
| */ | |||||
| StepNumber.prototype.start = function() { | |||||
| this._current = this._start - this._start % this._step; | |||||
| }; | |||||
| /** | |||||
| * Do a step, add the step size to the current value | |||||
| */ | |||||
| StepNumber.prototype.next = function () { | |||||
| this._current += this._step; | |||||
| }; | |||||
| /** | |||||
| * Returns true whether the end is reached | |||||
| * @return {boolean} True if the current value has passed the end value. | |||||
| */ | |||||
| StepNumber.prototype.end = function () { | |||||
| return (this._current > this._end); | |||||
| }; | |||||
| module.exports = StepNumber; | |||||
| @ -0,0 +1,10 @@ | |||||
| // Only load hammer.js when in a browser environment | |||||
| // (loading hammer.js in a node.js environment gives errors) | |||||
| if (typeof window !== 'undefined') { | |||||
| module.exports = require('hammerjs'); | |||||
| } | |||||
| else { | |||||
| module.exports = function () { | |||||
| throw Error('hammer.js is only available in a browser, not in node.js.'); | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,3 @@ | |||||
| // first check if moment.js is already loaded in the browser window, if so, | |||||
| // use this instance. Else, load via commonjs. | |||||
| module.exports = (typeof window !== 'undefined') && window['moment'] || require('moment'); | |||||
| @ -0,0 +1,826 @@ | |||||
| /** | |||||
| * Parse a text source containing data in DOT language into a JSON object. | |||||
| * The object contains two lists: one with nodes and one with edges. | |||||
| * | |||||
| * DOT language reference: http://www.graphviz.org/doc/info/lang.html | |||||
| * | |||||
| * @param {String} data Text containing a graph in DOT-notation | |||||
| * @return {Object} graph An object containing two parameters: | |||||
| * {Object[]} nodes | |||||
| * {Object[]} edges | |||||
| */ | |||||
| function parseDOT (data) { | |||||
| dot = data; | |||||
| return parseGraph(); | |||||
| } | |||||
| // token types enumeration | |||||
| var TOKENTYPE = { | |||||
| NULL : 0, | |||||
| DELIMITER : 1, | |||||
| IDENTIFIER: 2, | |||||
| UNKNOWN : 3 | |||||
| }; | |||||
| // map with all delimiters | |||||
| var DELIMITERS = { | |||||
| '{': true, | |||||
| '}': true, | |||||
| '[': true, | |||||
| ']': true, | |||||
| ';': true, | |||||
| '=': true, | |||||
| ',': true, | |||||
| '->': true, | |||||
| '--': true | |||||
| }; | |||||
| var dot = ''; // current dot file | |||||
| var index = 0; // current index in dot file | |||||
| var c = ''; // current token character in expr | |||||
| var token = ''; // current token | |||||
| var tokenType = TOKENTYPE.NULL; // type of the token | |||||
| /** | |||||
| * Get the first character from the dot file. | |||||
| * The character is stored into the char c. If the end of the dot file is | |||||
| * reached, the function puts an empty string in c. | |||||
| */ | |||||
| function first() { | |||||
| index = 0; | |||||
| c = dot.charAt(0); | |||||
| } | |||||
| /** | |||||
| * Get the next character from the dot file. | |||||
| * The character is stored into the char c. If the end of the dot file is | |||||
| * reached, the function puts an empty string in c. | |||||
| */ | |||||
| function next() { | |||||
| index++; | |||||
| c = dot.charAt(index); | |||||
| } | |||||
| /** | |||||
| * Preview the next character from the dot file. | |||||
| * @return {String} cNext | |||||
| */ | |||||
| function nextPreview() { | |||||
| return dot.charAt(index + 1); | |||||
| } | |||||
| /** | |||||
| * Test whether given character is alphabetic or numeric | |||||
| * @param {String} c | |||||
| * @return {Boolean} isAlphaNumeric | |||||
| */ | |||||
| var regexAlphaNumeric = /[a-zA-Z_0-9.:#]/; | |||||
| function isAlphaNumeric(c) { | |||||
| return regexAlphaNumeric.test(c); | |||||
| } | |||||
| /** | |||||
| * Merge all properties of object b into object b | |||||
| * @param {Object} a | |||||
| * @param {Object} b | |||||
| * @return {Object} a | |||||
| */ | |||||
| function merge (a, b) { | |||||
| if (!a) { | |||||
| a = {}; | |||||
| } | |||||
| if (b) { | |||||
| for (var name in b) { | |||||
| if (b.hasOwnProperty(name)) { | |||||
| a[name] = b[name]; | |||||
| } | |||||
| } | |||||
| } | |||||
| return a; | |||||
| } | |||||
| /** | |||||
| * Set a value in an object, where the provided parameter name can be a | |||||
| * path with nested parameters. For example: | |||||
| * | |||||
| * var obj = {a: 2}; | |||||
| * setValue(obj, 'b.c', 3); // obj = {a: 2, b: {c: 3}} | |||||
| * | |||||
| * @param {Object} obj | |||||
| * @param {String} path A parameter name or dot-separated parameter path, | |||||
| * like "color.highlight.border". | |||||
| * @param {*} value | |||||
| */ | |||||
| function setValue(obj, path, value) { | |||||
| var keys = path.split('.'); | |||||
| var o = obj; | |||||
| while (keys.length) { | |||||
| var key = keys.shift(); | |||||
| if (keys.length) { | |||||
| // this isn't the end point | |||||
| if (!o[key]) { | |||||
| o[key] = {}; | |||||
| } | |||||
| o = o[key]; | |||||
| } | |||||
| else { | |||||
| // this is the end point | |||||
| o[key] = value; | |||||
| } | |||||
| } | |||||
| } | |||||
| /** | |||||
| * Add a node to a graph object. If there is already a node with | |||||
| * the same id, their attributes will be merged. | |||||
| * @param {Object} graph | |||||
| * @param {Object} node | |||||
| */ | |||||
| function addNode(graph, node) { | |||||
| var i, len; | |||||
| var current = null; | |||||
| // find root graph (in case of subgraph) | |||||
| var graphs = [graph]; // list with all graphs from current graph to root graph | |||||
| var root = graph; | |||||
| while (root.parent) { | |||||
| graphs.push(root.parent); | |||||
| root = root.parent; | |||||
| } | |||||
| // find existing node (at root level) by its id | |||||
| if (root.nodes) { | |||||
| for (i = 0, len = root.nodes.length; i < len; i++) { | |||||
| if (node.id === root.nodes[i].id) { | |||||
| current = root.nodes[i]; | |||||
| break; | |||||
| } | |||||
| } | |||||
| } | |||||
| if (!current) { | |||||
| // this is a new node | |||||
| current = { | |||||
| id: node.id | |||||
| }; | |||||
| if (graph.node) { | |||||
| // clone default attributes | |||||
| current.attr = merge(current.attr, graph.node); | |||||
| } | |||||
| } | |||||
| // add node to this (sub)graph and all its parent graphs | |||||
| for (i = graphs.length - 1; i >= 0; i--) { | |||||
| var g = graphs[i]; | |||||
| if (!g.nodes) { | |||||
| g.nodes = []; | |||||
| } | |||||
| if (g.nodes.indexOf(current) == -1) { | |||||
| g.nodes.push(current); | |||||
| } | |||||
| } | |||||
| // merge attributes | |||||
| if (node.attr) { | |||||
| current.attr = merge(current.attr, node.attr); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * Add an edge to a graph object | |||||
| * @param {Object} graph | |||||
| * @param {Object} edge | |||||
| */ | |||||
| function addEdge(graph, edge) { | |||||
| if (!graph.edges) { | |||||
| graph.edges = []; | |||||
| } | |||||
| graph.edges.push(edge); | |||||
| if (graph.edge) { | |||||
| var attr = merge({}, graph.edge); // clone default attributes | |||||
| edge.attr = merge(attr, edge.attr); // merge attributes | |||||
| } | |||||
| } | |||||
| /** | |||||
| * Create an edge to a graph object | |||||
| * @param {Object} graph | |||||
| * @param {String | Number | Object} from | |||||
| * @param {String | Number | Object} to | |||||
| * @param {String} type | |||||
| * @param {Object | null} attr | |||||
| * @return {Object} edge | |||||
| */ | |||||
| function createEdge(graph, from, to, type, attr) { | |||||
| var edge = { | |||||
| from: from, | |||||
| to: to, | |||||
| type: type | |||||
| }; | |||||
| if (graph.edge) { | |||||
| edge.attr = merge({}, graph.edge); // clone default attributes | |||||
| } | |||||
| edge.attr = merge(edge.attr || {}, attr); // merge attributes | |||||
| return edge; | |||||
| } | |||||
| /** | |||||
| * Get next token in the current dot file. | |||||
| * The token and token type are available as token and tokenType | |||||
| */ | |||||
| function getToken() { | |||||
| tokenType = TOKENTYPE.NULL; | |||||
| token = ''; | |||||
| // skip over whitespaces | |||||
| while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter | |||||
| next(); | |||||
| } | |||||
| do { | |||||
| var isComment = false; | |||||
| // skip comment | |||||
| if (c == '#') { | |||||
| // find the previous non-space character | |||||
| var i = index - 1; | |||||
| while (dot.charAt(i) == ' ' || dot.charAt(i) == '\t') { | |||||
| i--; | |||||
| } | |||||
| if (dot.charAt(i) == '\n' || dot.charAt(i) == '') { | |||||
| // the # is at the start of a line, this is indeed a line comment | |||||
| while (c != '' && c != '\n') { | |||||
| next(); | |||||
| } | |||||
| isComment = true; | |||||
| } | |||||
| } | |||||
| if (c == '/' && nextPreview() == '/') { | |||||
| // skip line comment | |||||
| while (c != '' && c != '\n') { | |||||
| next(); | |||||
| } | |||||
| isComment = true; | |||||
| } | |||||
| if (c == '/' && nextPreview() == '*') { | |||||
| // skip block comment | |||||
| while (c != '') { | |||||
| if (c == '*' && nextPreview() == '/') { | |||||
| // end of block comment found. skip these last two characters | |||||
| next(); | |||||
| next(); | |||||
| break; | |||||
| } | |||||
| else { | |||||
| next(); | |||||
| } | |||||
| } | |||||
| isComment = true; | |||||
| } | |||||
| // skip over whitespaces | |||||
| while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter | |||||
| next(); | |||||
| } | |||||
| } | |||||
| while (isComment); | |||||
| // check for end of dot file | |||||
| if (c == '') { | |||||
| // token is still empty | |||||
| tokenType = TOKENTYPE.DELIMITER; | |||||
| return; | |||||
| } | |||||
| // check for delimiters consisting of 2 characters | |||||
| var c2 = c + nextPreview(); | |||||
| if (DELIMITERS[c2]) { | |||||
| tokenType = TOKENTYPE.DELIMITER; | |||||
| token = c2; | |||||
| next(); | |||||
| next(); | |||||
| return; | |||||
| } | |||||
| // check for delimiters consisting of 1 character | |||||
| if (DELIMITERS[c]) { | |||||
| tokenType = TOKENTYPE.DELIMITER; | |||||
| token = c; | |||||
| next(); | |||||
| return; | |||||
| } | |||||
| // check for an identifier (number or string) | |||||
| // TODO: more precise parsing of numbers/strings (and the port separator ':') | |||||
| if (isAlphaNumeric(c) || c == '-') { | |||||
| token += c; | |||||
| next(); | |||||
| while (isAlphaNumeric(c)) { | |||||
| token += c; | |||||
| next(); | |||||
| } | |||||
| if (token == 'false') { | |||||
| token = false; // convert to boolean | |||||
| } | |||||
| else if (token == 'true') { | |||||
| token = true; // convert to boolean | |||||
| } | |||||
| else if (!isNaN(Number(token))) { | |||||
| token = Number(token); // convert to number | |||||
| } | |||||
| tokenType = TOKENTYPE.IDENTIFIER; | |||||
| return; | |||||
| } | |||||
| // check for a string enclosed by double quotes | |||||
| if (c == '"') { | |||||
| next(); | |||||
| while (c != '' && (c != '"' || (c == '"' && nextPreview() == '"'))) { | |||||
| token += c; | |||||
| if (c == '"') { // skip the escape character | |||||
| next(); | |||||
| } | |||||
| next(); | |||||
| } | |||||
| if (c != '"') { | |||||
| throw newSyntaxError('End of string " expected'); | |||||
| } | |||||
| next(); | |||||
| tokenType = TOKENTYPE.IDENTIFIER; | |||||
| return; | |||||
| } | |||||
| // something unknown is found, wrong characters, a syntax error | |||||
| tokenType = TOKENTYPE.UNKNOWN; | |||||
| while (c != '') { | |||||
| token += c; | |||||
| next(); | |||||
| } | |||||
| throw new SyntaxError('Syntax error in part "' + chop(token, 30) + '"'); | |||||
| } | |||||
| /** | |||||
| * Parse a graph. | |||||
| * @returns {Object} graph | |||||
| */ | |||||
| function parseGraph() { | |||||
| var graph = {}; | |||||
| first(); | |||||
| getToken(); | |||||
| // optional strict keyword | |||||
| if (token == 'strict') { | |||||
| graph.strict = true; | |||||
| getToken(); | |||||
| } | |||||
| // graph or digraph keyword | |||||
| if (token == 'graph' || token == 'digraph') { | |||||
| graph.type = token; | |||||
| getToken(); | |||||
| } | |||||
| // optional graph id | |||||
| if (tokenType == TOKENTYPE.IDENTIFIER) { | |||||
| graph.id = token; | |||||
| getToken(); | |||||
| } | |||||
| // open angle bracket | |||||
| if (token != '{') { | |||||
| throw newSyntaxError('Angle bracket { expected'); | |||||
| } | |||||
| getToken(); | |||||
| // statements | |||||
| parseStatements(graph); | |||||
| // close angle bracket | |||||
| if (token != '}') { | |||||
| throw newSyntaxError('Angle bracket } expected'); | |||||
| } | |||||
| getToken(); | |||||
| // end of file | |||||
| if (token !== '') { | |||||
| throw newSyntaxError('End of file expected'); | |||||
| } | |||||
| getToken(); | |||||
| // remove temporary default properties | |||||
| delete graph.node; | |||||
| delete graph.edge; | |||||
| delete graph.graph; | |||||
| return graph; | |||||
| } | |||||
| /** | |||||
| * Parse a list with statements. | |||||
| * @param {Object} graph | |||||
| */ | |||||
| function parseStatements (graph) { | |||||
| while (token !== '' && token != '}') { | |||||
| parseStatement(graph); | |||||
| if (token == ';') { | |||||
| getToken(); | |||||
| } | |||||
| } | |||||
| } | |||||
| /** | |||||
| * Parse a single statement. Can be a an attribute statement, node | |||||
| * statement, a series of node statements and edge statements, or a | |||||
| * parameter. | |||||
| * @param {Object} graph | |||||
| */ | |||||
| function parseStatement(graph) { | |||||
| // parse subgraph | |||||
| var subgraph = parseSubgraph(graph); | |||||
| if (subgraph) { | |||||
| // edge statements | |||||
| parseEdge(graph, subgraph); | |||||
| return; | |||||
| } | |||||
| // parse an attribute statement | |||||
| var attr = parseAttributeStatement(graph); | |||||
| if (attr) { | |||||
| return; | |||||
| } | |||||
| // parse node | |||||
| if (tokenType != TOKENTYPE.IDENTIFIER) { | |||||
| throw newSyntaxError('Identifier expected'); | |||||
| } | |||||
| var id = token; // id can be a string or a number | |||||
| getToken(); | |||||
| if (token == '=') { | |||||
| // id statement | |||||
| getToken(); | |||||
| if (tokenType != TOKENTYPE.IDENTIFIER) { | |||||
| throw newSyntaxError('Identifier expected'); | |||||
| } | |||||
| graph[id] = token; | |||||
| getToken(); | |||||
| // TODO: implement comma separated list with "a_list: ID=ID [','] [a_list] " | |||||
| } | |||||
| else { | |||||
| parseNodeStatement(graph, id); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * Parse a subgraph | |||||
| * @param {Object} graph parent graph object | |||||
| * @return {Object | null} subgraph | |||||
| */ | |||||
| function parseSubgraph (graph) { | |||||
| var subgraph = null; | |||||
| // optional subgraph keyword | |||||
| if (token == 'subgraph') { | |||||
| subgraph = {}; | |||||
| subgraph.type = 'subgraph'; | |||||
| getToken(); | |||||
| // optional graph id | |||||
| if (tokenType == TOKENTYPE.IDENTIFIER) { | |||||
| subgraph.id = token; | |||||
| getToken(); | |||||
| } | |||||
| } | |||||
| // open angle bracket | |||||
| if (token == '{') { | |||||
| getToken(); | |||||
| if (!subgraph) { | |||||
| subgraph = {}; | |||||
| } | |||||
| subgraph.parent = graph; | |||||
| subgraph.node = graph.node; | |||||
| subgraph.edge = graph.edge; | |||||
| subgraph.graph = graph.graph; | |||||
| // statements | |||||
| parseStatements(subgraph); | |||||
| // close angle bracket | |||||
| if (token != '}') { | |||||
| throw newSyntaxError('Angle bracket } expected'); | |||||
| } | |||||
| getToken(); | |||||
| // remove temporary default properties | |||||
| delete subgraph.node; | |||||
| delete subgraph.edge; | |||||
| delete subgraph.graph; | |||||
| delete subgraph.parent; | |||||
| // register at the parent graph | |||||
| if (!graph.subgraphs) { | |||||
| graph.subgraphs = []; | |||||
| } | |||||
| graph.subgraphs.push(subgraph); | |||||
| } | |||||
| return subgraph; | |||||
| } | |||||
| /** | |||||
| * parse an attribute statement like "node [shape=circle fontSize=16]". | |||||
| * Available keywords are 'node', 'edge', 'graph'. | |||||
| * The previous list with default attributes will be replaced | |||||
| * @param {Object} graph | |||||
| * @returns {String | null} keyword Returns the name of the parsed attribute | |||||
| * (node, edge, graph), or null if nothing | |||||
| * is parsed. | |||||
| */ | |||||
| function parseAttributeStatement (graph) { | |||||
| // attribute statements | |||||
| if (token == 'node') { | |||||
| getToken(); | |||||
| // node attributes | |||||
| graph.node = parseAttributeList(); | |||||
| return 'node'; | |||||
| } | |||||
| else if (token == 'edge') { | |||||
| getToken(); | |||||
| // edge attributes | |||||
| graph.edge = parseAttributeList(); | |||||
| return 'edge'; | |||||
| } | |||||
| else if (token == 'graph') { | |||||
| getToken(); | |||||
| // graph attributes | |||||
| graph.graph = parseAttributeList(); | |||||
| return 'graph'; | |||||
| } | |||||
| return null; | |||||
| } | |||||
| /** | |||||
| * parse a node statement | |||||
| * @param {Object} graph | |||||
| * @param {String | Number} id | |||||
| */ | |||||
| function parseNodeStatement(graph, id) { | |||||
| // node statement | |||||
| var node = { | |||||
| id: id | |||||
| }; | |||||
| var attr = parseAttributeList(); | |||||
| if (attr) { | |||||
| node.attr = attr; | |||||
| } | |||||
| addNode(graph, node); | |||||
| // edge statements | |||||
| parseEdge(graph, id); | |||||
| } | |||||
| /** | |||||
| * Parse an edge or a series of edges | |||||
| * @param {Object} graph | |||||
| * @param {String | Number} from Id of the from node | |||||
| */ | |||||
| function parseEdge(graph, from) { | |||||
| while (token == '->' || token == '--') { | |||||
| var to; | |||||
| var type = token; | |||||
| getToken(); | |||||
| var subgraph = parseSubgraph(graph); | |||||
| if (subgraph) { | |||||
| to = subgraph; | |||||
| } | |||||
| else { | |||||
| if (tokenType != TOKENTYPE.IDENTIFIER) { | |||||
| throw newSyntaxError('Identifier or subgraph expected'); | |||||
| } | |||||
| to = token; | |||||
| addNode(graph, { | |||||
| id: to | |||||
| }); | |||||
| getToken(); | |||||
| } | |||||
| // parse edge attributes | |||||
| var attr = parseAttributeList(); | |||||
| // create edge | |||||
| var edge = createEdge(graph, from, to, type, attr); | |||||
| addEdge(graph, edge); | |||||
| from = to; | |||||
| } | |||||
| } | |||||
| /** | |||||
| * Parse a set with attributes, | |||||
| * for example [label="1.000", shape=solid] | |||||
| * @return {Object | null} attr | |||||
| */ | |||||
| function parseAttributeList() { | |||||
| var attr = null; | |||||
| while (token == '[') { | |||||
| getToken(); | |||||
| attr = {}; | |||||
| while (token !== '' && token != ']') { | |||||
| if (tokenType != TOKENTYPE.IDENTIFIER) { | |||||
| throw newSyntaxError('Attribute name expected'); | |||||
| } | |||||
| var name = token; | |||||
| getToken(); | |||||
| if (token != '=') { | |||||
| throw newSyntaxError('Equal sign = expected'); | |||||
| } | |||||
| getToken(); | |||||
| if (tokenType != TOKENTYPE.IDENTIFIER) { | |||||
| throw newSyntaxError('Attribute value expected'); | |||||
| } | |||||
| var value = token; | |||||
| setValue(attr, name, value); // name can be a path | |||||
| getToken(); | |||||
| if (token ==',') { | |||||
| getToken(); | |||||
| } | |||||
| } | |||||
| if (token != ']') { | |||||
| throw newSyntaxError('Bracket ] expected'); | |||||
| } | |||||
| getToken(); | |||||
| } | |||||
| return attr; | |||||
| } | |||||
| /** | |||||
| * Create a syntax error with extra information on current token and index. | |||||
| * @param {String} message | |||||
| * @returns {SyntaxError} err | |||||
| */ | |||||
| function newSyntaxError(message) { | |||||
| return new SyntaxError(message + ', got "' + chop(token, 30) + '" (char ' + index + ')'); | |||||
| } | |||||
| /** | |||||
| * Chop off text after a maximum length | |||||
| * @param {String} text | |||||
| * @param {Number} maxLength | |||||
| * @returns {String} | |||||
| */ | |||||
| function chop (text, maxLength) { | |||||
| return (text.length <= maxLength) ? text : (text.substr(0, 27) + '...'); | |||||
| } | |||||
| /** | |||||
| * Execute a function fn for each pair of elements in two arrays | |||||
| * @param {Array | *} array1 | |||||
| * @param {Array | *} array2 | |||||
| * @param {function} fn | |||||
| */ | |||||
| function forEach2(array1, array2, fn) { | |||||
| if (array1 instanceof Array) { | |||||
| array1.forEach(function (elem1) { | |||||
| if (array2 instanceof Array) { | |||||
| array2.forEach(function (elem2) { | |||||
| fn(elem1, elem2); | |||||
| }); | |||||
| } | |||||
| else { | |||||
| fn(elem1, array2); | |||||
| } | |||||
| }); | |||||
| } | |||||
| else { | |||||
| if (array2 instanceof Array) { | |||||
| array2.forEach(function (elem2) { | |||||
| fn(array1, elem2); | |||||
| }); | |||||
| } | |||||
| else { | |||||
| fn(array1, array2); | |||||
| } | |||||
| } | |||||
| } | |||||
| /** | |||||
| * Convert a string containing a graph in DOT language into a map containing | |||||
| * with nodes and edges in the format of graph. | |||||
| * @param {String} data Text containing a graph in DOT-notation | |||||
| * @return {Object} graphData | |||||
| */ | |||||
| function DOTToGraph (data) { | |||||
| // parse the DOT file | |||||
| var dotData = parseDOT(data); | |||||
| var graphData = { | |||||
| nodes: [], | |||||
| edges: [], | |||||
| options: {} | |||||
| }; | |||||
| // copy the nodes | |||||
| if (dotData.nodes) { | |||||
| dotData.nodes.forEach(function (dotNode) { | |||||
| var graphNode = { | |||||
| id: dotNode.id, | |||||
| label: String(dotNode.label || dotNode.id) | |||||
| }; | |||||
| merge(graphNode, dotNode.attr); | |||||
| if (graphNode.image) { | |||||
| graphNode.shape = 'image'; | |||||
| } | |||||
| graphData.nodes.push(graphNode); | |||||
| }); | |||||
| } | |||||
| // copy the edges | |||||
| if (dotData.edges) { | |||||
| /** | |||||
| * Convert an edge in DOT format to an edge with VisGraph format | |||||
| * @param {Object} dotEdge | |||||
| * @returns {Object} graphEdge | |||||
| */ | |||||
| function convertEdge(dotEdge) { | |||||
| var graphEdge = { | |||||
| from: dotEdge.from, | |||||
| to: dotEdge.to | |||||
| }; | |||||
| merge(graphEdge, dotEdge.attr); | |||||
| graphEdge.style = (dotEdge.type == '->') ? 'arrow' : 'line'; | |||||
| return graphEdge; | |||||
| } | |||||
| dotData.edges.forEach(function (dotEdge) { | |||||
| var from, to; | |||||
| if (dotEdge.from instanceof Object) { | |||||
| from = dotEdge.from.nodes; | |||||
| } | |||||
| else { | |||||
| from = { | |||||
| id: dotEdge.from | |||||
| } | |||||
| } | |||||
| if (dotEdge.to instanceof Object) { | |||||
| to = dotEdge.to.nodes; | |||||
| } | |||||
| else { | |||||
| to = { | |||||
| id: dotEdge.to | |||||
| } | |||||
| } | |||||
| if (dotEdge.from instanceof Object && dotEdge.from.edges) { | |||||
| dotEdge.from.edges.forEach(function (subEdge) { | |||||
| var graphEdge = convertEdge(subEdge); | |||||
| graphData.edges.push(graphEdge); | |||||
| }); | |||||
| } | |||||
| forEach2(from, to, function (from, to) { | |||||
| var subEdge = createEdge(graphData, from.id, to.id, dotEdge.type, dotEdge.attr); | |||||
| var graphEdge = convertEdge(subEdge); | |||||
| graphData.edges.push(graphEdge); | |||||
| }); | |||||
| if (dotEdge.to instanceof Object && dotEdge.to.edges) { | |||||
| dotEdge.to.edges.forEach(function (subEdge) { | |||||
| var graphEdge = convertEdge(subEdge); | |||||
| graphData.edges.push(graphEdge); | |||||
| }); | |||||
| } | |||||
| }); | |||||
| } | |||||
| // copy the options | |||||
| if (dotData.attr) { | |||||
| graphData.options = dotData.attr; | |||||
| } | |||||
| return graphData; | |||||
| } | |||||
| // exports | |||||
| exports.parseDOT = parseDOT; | |||||
| exports.DOTToGraph = DOTToGraph; | |||||
| @ -0,0 +1,304 @@ | |||||
| exports._resetLevels = function() { | |||||
| for (var nodeId in this.nodes) { | |||||
| if (this.nodes.hasOwnProperty(nodeId)) { | |||||
| var node = this.nodes[nodeId]; | |||||
| if (node.preassignedLevel == false) { | |||||
| node.level = -1; | |||||
| } | |||||
| } | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * This is the main function to layout the nodes in a hierarchical way. | |||||
| * It checks if the node details are supplied correctly | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._setupHierarchicalLayout = function() { | |||||
| if (this.constants.hierarchicalLayout.enabled == true && this.nodeIndices.length > 0) { | |||||
| if (this.constants.hierarchicalLayout.direction == "RL" || this.constants.hierarchicalLayout.direction == "DU") { | |||||
| this.constants.hierarchicalLayout.levelSeparation *= -1; | |||||
| } | |||||
| else { | |||||
| this.constants.hierarchicalLayout.levelSeparation = Math.abs(this.constants.hierarchicalLayout.levelSeparation); | |||||
| } | |||||
| // get the size of the largest hubs and check if the user has defined a level for a node. | |||||
| var hubsize = 0; | |||||
| var node, nodeId; | |||||
| var definedLevel = false; | |||||
| var undefinedLevel = false; | |||||
| for (nodeId in this.nodes) { | |||||
| if (this.nodes.hasOwnProperty(nodeId)) { | |||||
| node = this.nodes[nodeId]; | |||||
| if (node.level != -1) { | |||||
| definedLevel = true; | |||||
| } | |||||
| else { | |||||
| undefinedLevel = true; | |||||
| } | |||||
| if (hubsize < node.edges.length) { | |||||
| hubsize = node.edges.length; | |||||
| } | |||||
| } | |||||
| } | |||||
| // if the user defined some levels but not all, alert and run without hierarchical layout | |||||
| if (undefinedLevel == true && definedLevel == true) { | |||||
| alert("To use the hierarchical layout, nodes require either no predefined levels or levels have to be defined for all nodes."); | |||||
| this.zoomExtent(true,this.constants.clustering.enabled); | |||||
| if (!this.constants.clustering.enabled) { | |||||
| this.start(); | |||||
| } | |||||
| } | |||||
| else { | |||||
| // setup the system to use hierarchical method. | |||||
| this._changeConstants(); | |||||
| // define levels if undefined by the users. Based on hubsize | |||||
| if (undefinedLevel == true) { | |||||
| this._determineLevels(hubsize); | |||||
| } | |||||
| // check the distribution of the nodes per level. | |||||
| var distribution = this._getDistribution(); | |||||
| // place the nodes on the canvas. This also stablilizes the system. | |||||
| this._placeNodesByHierarchy(distribution); | |||||
| // start the simulation. | |||||
| this.start(); | |||||
| } | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * This function places the nodes on the canvas based on the hierarchial distribution. | |||||
| * | |||||
| * @param {Object} distribution | obtained by the function this._getDistribution() | |||||
| * @private | |||||
| */ | |||||
| exports._placeNodesByHierarchy = function(distribution) { | |||||
| var nodeId, node; | |||||
| // start placing all the level 0 nodes first. Then recursively position their branches. | |||||
| for (nodeId in distribution[0].nodes) { | |||||
| if (distribution[0].nodes.hasOwnProperty(nodeId)) { | |||||
| node = distribution[0].nodes[nodeId]; | |||||
| if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") { | |||||
| if (node.xFixed) { | |||||
| node.x = distribution[0].minPos; | |||||
| node.xFixed = false; | |||||
| distribution[0].minPos += distribution[0].nodeSpacing; | |||||
| } | |||||
| } | |||||
| else { | |||||
| if (node.yFixed) { | |||||
| node.y = distribution[0].minPos; | |||||
| node.yFixed = false; | |||||
| distribution[0].minPos += distribution[0].nodeSpacing; | |||||
| } | |||||
| } | |||||
| this._placeBranchNodes(node.edges,node.id,distribution,node.level); | |||||
| } | |||||
| } | |||||
| // stabilize the system after positioning. This function calls zoomExtent. | |||||
| this._stabilize(); | |||||
| }; | |||||
| /** | |||||
| * This function get the distribution of levels based on hubsize | |||||
| * | |||||
| * @returns {Object} | |||||
| * @private | |||||
| */ | |||||
| exports._getDistribution = function() { | |||||
| var distribution = {}; | |||||
| var nodeId, node, level; | |||||
| // we fix Y because the hierarchy is vertical, we fix X so we do not give a node an x position for a second time. | |||||
| // the fix of X is removed after the x value has been set. | |||||
| for (nodeId in this.nodes) { | |||||
| if (this.nodes.hasOwnProperty(nodeId)) { | |||||
| node = this.nodes[nodeId]; | |||||
| node.xFixed = true; | |||||
| node.yFixed = true; | |||||
| if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") { | |||||
| node.y = this.constants.hierarchicalLayout.levelSeparation*node.level; | |||||
| } | |||||
| else { | |||||
| node.x = this.constants.hierarchicalLayout.levelSeparation*node.level; | |||||
| } | |||||
| if (!distribution.hasOwnProperty(node.level)) { | |||||
| distribution[node.level] = {amount: 0, nodes: {}, minPos:0, nodeSpacing:0}; | |||||
| } | |||||
| distribution[node.level].amount += 1; | |||||
| distribution[node.level].nodes[node.id] = node; | |||||
| } | |||||
| } | |||||
| // determine the largest amount of nodes of all levels | |||||
| var maxCount = 0; | |||||
| for (level in distribution) { | |||||
| if (distribution.hasOwnProperty(level)) { | |||||
| if (maxCount < distribution[level].amount) { | |||||
| maxCount = distribution[level].amount; | |||||
| } | |||||
| } | |||||
| } | |||||
| // set the initial position and spacing of each nodes accordingly | |||||
| for (level in distribution) { | |||||
| if (distribution.hasOwnProperty(level)) { | |||||
| distribution[level].nodeSpacing = (maxCount + 1) * this.constants.hierarchicalLayout.nodeSpacing; | |||||
| distribution[level].nodeSpacing /= (distribution[level].amount + 1); | |||||
| distribution[level].minPos = distribution[level].nodeSpacing - (0.5 * (distribution[level].amount + 1) * distribution[level].nodeSpacing); | |||||
| } | |||||
| } | |||||
| return distribution; | |||||
| }; | |||||
| /** | |||||
| * this function allocates nodes in levels based on the recursive branching from the largest hubs. | |||||
| * | |||||
| * @param hubsize | |||||
| * @private | |||||
| */ | |||||
| exports._determineLevels = function(hubsize) { | |||||
| var nodeId, node; | |||||
| // determine hubs | |||||
| for (nodeId in this.nodes) { | |||||
| if (this.nodes.hasOwnProperty(nodeId)) { | |||||
| node = this.nodes[nodeId]; | |||||
| if (node.edges.length == hubsize) { | |||||
| node.level = 0; | |||||
| } | |||||
| } | |||||
| } | |||||
| // branch from hubs | |||||
| for (nodeId in this.nodes) { | |||||
| if (this.nodes.hasOwnProperty(nodeId)) { | |||||
| node = this.nodes[nodeId]; | |||||
| if (node.level == 0) { | |||||
| this._setLevel(1,node.edges,node.id); | |||||
| } | |||||
| } | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Since hierarchical layout does not support: | |||||
| * - smooth curves (based on the physics), | |||||
| * - clustering (based on dynamic node counts) | |||||
| * | |||||
| * We disable both features so there will be no problems. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._changeConstants = function() { | |||||
| this.constants.clustering.enabled = false; | |||||
| this.constants.physics.barnesHut.enabled = false; | |||||
| this.constants.physics.hierarchicalRepulsion.enabled = true; | |||||
| this._loadSelectedForceSolver(); | |||||
| this.constants.smoothCurves = false; | |||||
| this._configureSmoothCurves(); | |||||
| }; | |||||
| /** | |||||
| * This is a recursively called function to enumerate the branches from the largest hubs and place the nodes | |||||
| * on a X position that ensures there will be no overlap. | |||||
| * | |||||
| * @param edges | |||||
| * @param parentId | |||||
| * @param distribution | |||||
| * @param parentLevel | |||||
| * @private | |||||
| */ | |||||
| exports._placeBranchNodes = function(edges, parentId, distribution, parentLevel) { | |||||
| for (var i = 0; i < edges.length; i++) { | |||||
| var childNode = null; | |||||
| if (edges[i].toId == parentId) { | |||||
| childNode = edges[i].from; | |||||
| } | |||||
| else { | |||||
| childNode = edges[i].to; | |||||
| } | |||||
| // if a node is conneceted to another node on the same level (or higher (means lower level))!, this is not handled here. | |||||
| var nodeMoved = false; | |||||
| if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") { | |||||
| if (childNode.xFixed && childNode.level > parentLevel) { | |||||
| childNode.xFixed = false; | |||||
| childNode.x = distribution[childNode.level].minPos; | |||||
| nodeMoved = true; | |||||
| } | |||||
| } | |||||
| else { | |||||
| if (childNode.yFixed && childNode.level > parentLevel) { | |||||
| childNode.yFixed = false; | |||||
| childNode.y = distribution[childNode.level].minPos; | |||||
| nodeMoved = true; | |||||
| } | |||||
| } | |||||
| if (nodeMoved == true) { | |||||
| distribution[childNode.level].minPos += distribution[childNode.level].nodeSpacing; | |||||
| if (childNode.edges.length > 1) { | |||||
| this._placeBranchNodes(childNode.edges,childNode.id,distribution,childNode.level); | |||||
| } | |||||
| } | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * this function is called recursively to enumerate the barnches of the largest hubs and give each node a level. | |||||
| * | |||||
| * @param level | |||||
| * @param edges | |||||
| * @param parentId | |||||
| * @private | |||||
| */ | |||||
| exports._setLevel = function(level, edges, parentId) { | |||||
| for (var i = 0; i < edges.length; i++) { | |||||
| var childNode = null; | |||||
| if (edges[i].toId == parentId) { | |||||
| childNode = edges[i].from; | |||||
| } | |||||
| else { | |||||
| childNode = edges[i].to; | |||||
| } | |||||
| if (childNode.level == -1 || childNode.level > level) { | |||||
| childNode.level = level; | |||||
| if (edges.length > 1) { | |||||
| this._setLevel(level+1, childNode.edges, childNode.id); | |||||
| } | |||||
| } | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Unfix nodes | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._restoreNodes = function() { | |||||
| for (var nodeId in this.nodes) { | |||||
| if (this.nodes.hasOwnProperty(nodeId)) { | |||||
| this.nodes[nodeId].xFixed = false; | |||||
| this.nodes[nodeId].yFixed = false; | |||||
| } | |||||
| } | |||||
| }; | |||||
| @ -0,0 +1,571 @@ | |||||
| var util = require('../../util'); | |||||
| /** | |||||
| * clears the toolbar div element of children | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._clearManipulatorBar = function() { | |||||
| while (this.manipulationDiv.hasChildNodes()) { | |||||
| this.manipulationDiv.removeChild(this.manipulationDiv.firstChild); | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Manipulation UI temporarily overloads certain functions to extend or replace them. To be able to restore | |||||
| * these functions to their original functionality, we saved them in this.cachedFunctions. | |||||
| * This function restores these functions to their original function. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._restoreOverloadedFunctions = function() { | |||||
| for (var functionName in this.cachedFunctions) { | |||||
| if (this.cachedFunctions.hasOwnProperty(functionName)) { | |||||
| this[functionName] = this.cachedFunctions[functionName]; | |||||
| } | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Enable or disable edit-mode. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._toggleEditMode = function() { | |||||
| this.editMode = !this.editMode; | |||||
| var toolbar = document.getElementById("network-manipulationDiv"); | |||||
| var closeDiv = document.getElementById("network-manipulation-closeDiv"); | |||||
| var editModeDiv = document.getElementById("network-manipulation-editMode"); | |||||
| if (this.editMode == true) { | |||||
| toolbar.style.display="block"; | |||||
| closeDiv.style.display="block"; | |||||
| editModeDiv.style.display="none"; | |||||
| closeDiv.onclick = this._toggleEditMode.bind(this); | |||||
| } | |||||
| else { | |||||
| toolbar.style.display="none"; | |||||
| closeDiv.style.display="none"; | |||||
| editModeDiv.style.display="block"; | |||||
| closeDiv.onclick = null; | |||||
| } | |||||
| this._createManipulatorBar() | |||||
| }; | |||||
| /** | |||||
| * main function, creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._createManipulatorBar = function() { | |||||
| // remove bound functions | |||||
| if (this.boundFunction) { | |||||
| this.off('select', this.boundFunction); | |||||
| } | |||||
| if (this.edgeBeingEdited !== undefined) { | |||||
| this.edgeBeingEdited._disableControlNodes(); | |||||
| this.edgeBeingEdited = undefined; | |||||
| this.selectedControlNode = null; | |||||
| } | |||||
| // restore overloaded functions | |||||
| this._restoreOverloadedFunctions(); | |||||
| // resume calculation | |||||
| this.freezeSimulation = false; | |||||
| // reset global variables | |||||
| this.blockConnectingEdgeSelection = false; | |||||
| this.forceAppendSelection = false; | |||||
| if (this.editMode == true) { | |||||
| while (this.manipulationDiv.hasChildNodes()) { | |||||
| this.manipulationDiv.removeChild(this.manipulationDiv.firstChild); | |||||
| } | |||||
| // add the icons to the manipulator div | |||||
| this.manipulationDiv.innerHTML = "" + | |||||
| "<span class='network-manipulationUI add' id='network-manipulate-addNode'>" + | |||||
| "<span class='network-manipulationLabel'>"+this.constants.labels['add'] +"</span></span>" + | |||||
| "<div class='network-seperatorLine'></div>" + | |||||
| "<span class='network-manipulationUI connect' id='network-manipulate-connectNode'>" + | |||||
| "<span class='network-manipulationLabel'>"+this.constants.labels['link'] +"</span></span>"; | |||||
| if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) { | |||||
| this.manipulationDiv.innerHTML += "" + | |||||
| "<div class='network-seperatorLine'></div>" + | |||||
| "<span class='network-manipulationUI edit' id='network-manipulate-editNode'>" + | |||||
| "<span class='network-manipulationLabel'>"+this.constants.labels['editNode'] +"</span></span>"; | |||||
| } | |||||
| else if (this._getSelectedEdgeCount() == 1 && this._getSelectedNodeCount() == 0) { | |||||
| this.manipulationDiv.innerHTML += "" + | |||||
| "<div class='network-seperatorLine'></div>" + | |||||
| "<span class='network-manipulationUI edit' id='network-manipulate-editEdge'>" + | |||||
| "<span class='network-manipulationLabel'>"+this.constants.labels['editEdge'] +"</span></span>"; | |||||
| } | |||||
| if (this._selectionIsEmpty() == false) { | |||||
| this.manipulationDiv.innerHTML += "" + | |||||
| "<div class='network-seperatorLine'></div>" + | |||||
| "<span class='network-manipulationUI delete' id='network-manipulate-delete'>" + | |||||
| "<span class='network-manipulationLabel'>"+this.constants.labels['del'] +"</span></span>"; | |||||
| } | |||||
| // bind the icons | |||||
| var addNodeButton = document.getElementById("network-manipulate-addNode"); | |||||
| addNodeButton.onclick = this._createAddNodeToolbar.bind(this); | |||||
| var addEdgeButton = document.getElementById("network-manipulate-connectNode"); | |||||
| addEdgeButton.onclick = this._createAddEdgeToolbar.bind(this); | |||||
| if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) { | |||||
| var editButton = document.getElementById("network-manipulate-editNode"); | |||||
| editButton.onclick = this._editNode.bind(this); | |||||
| } | |||||
| else if (this._getSelectedEdgeCount() == 1 && this._getSelectedNodeCount() == 0) { | |||||
| var editButton = document.getElementById("network-manipulate-editEdge"); | |||||
| editButton.onclick = this._createEditEdgeToolbar.bind(this); | |||||
| } | |||||
| if (this._selectionIsEmpty() == false) { | |||||
| var deleteButton = document.getElementById("network-manipulate-delete"); | |||||
| deleteButton.onclick = this._deleteSelected.bind(this); | |||||
| } | |||||
| var closeDiv = document.getElementById("network-manipulation-closeDiv"); | |||||
| closeDiv.onclick = this._toggleEditMode.bind(this); | |||||
| this.boundFunction = this._createManipulatorBar.bind(this); | |||||
| this.on('select', this.boundFunction); | |||||
| } | |||||
| else { | |||||
| this.editModeDiv.innerHTML = "" + | |||||
| "<span class='network-manipulationUI edit editmode' id='network-manipulate-editModeButton'>" + | |||||
| "<span class='network-manipulationLabel'>" + this.constants.labels['edit'] + "</span></span>"; | |||||
| var editModeButton = document.getElementById("network-manipulate-editModeButton"); | |||||
| editModeButton.onclick = this._toggleEditMode.bind(this); | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Create the toolbar for adding Nodes | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._createAddNodeToolbar = function() { | |||||
| // clear the toolbar | |||||
| this._clearManipulatorBar(); | |||||
| if (this.boundFunction) { | |||||
| this.off('select', this.boundFunction); | |||||
| } | |||||
| // create the toolbar contents | |||||
| this.manipulationDiv.innerHTML = "" + | |||||
| "<span class='network-manipulationUI back' id='network-manipulate-back'>" + | |||||
| "<span class='network-manipulationLabel'>" + this.constants.labels['back'] + " </span></span>" + | |||||
| "<div class='network-seperatorLine'></div>" + | |||||
| "<span class='network-manipulationUI none' id='network-manipulate-back'>" + | |||||
| "<span id='network-manipulatorLabel' class='network-manipulationLabel'>" + this.constants.labels['addDescription'] + "</span></span>"; | |||||
| // bind the icon | |||||
| var backButton = document.getElementById("network-manipulate-back"); | |||||
| backButton.onclick = this._createManipulatorBar.bind(this); | |||||
| // we use the boundFunction so we can reference it when we unbind it from the "select" event. | |||||
| this.boundFunction = this._addNode.bind(this); | |||||
| this.on('select', this.boundFunction); | |||||
| }; | |||||
| /** | |||||
| * create the toolbar to connect nodes | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._createAddEdgeToolbar = function() { | |||||
| // clear the toolbar | |||||
| this._clearManipulatorBar(); | |||||
| this._unselectAll(true); | |||||
| this.freezeSimulation = true; | |||||
| if (this.boundFunction) { | |||||
| this.off('select', this.boundFunction); | |||||
| } | |||||
| this._unselectAll(); | |||||
| this.forceAppendSelection = false; | |||||
| this.blockConnectingEdgeSelection = true; | |||||
| this.manipulationDiv.innerHTML = "" + | |||||
| "<span class='network-manipulationUI back' id='network-manipulate-back'>" + | |||||
| "<span class='network-manipulationLabel'>" + this.constants.labels['back'] + " </span></span>" + | |||||
| "<div class='network-seperatorLine'></div>" + | |||||
| "<span class='network-manipulationUI none' id='network-manipulate-back'>" + | |||||
| "<span id='network-manipulatorLabel' class='network-manipulationLabel'>" + this.constants.labels['linkDescription'] + "</span></span>"; | |||||
| // bind the icon | |||||
| var backButton = document.getElementById("network-manipulate-back"); | |||||
| backButton.onclick = this._createManipulatorBar.bind(this); | |||||
| // we use the boundFunction so we can reference it when we unbind it from the "select" event. | |||||
| this.boundFunction = this._handleConnect.bind(this); | |||||
| this.on('select', this.boundFunction); | |||||
| // temporarily overload functions | |||||
| this.cachedFunctions["_handleTouch"] = this._handleTouch; | |||||
| this.cachedFunctions["_handleOnRelease"] = this._handleOnRelease; | |||||
| this._handleTouch = this._handleConnect; | |||||
| this._handleOnRelease = this._finishConnect; | |||||
| // redraw to show the unselect | |||||
| this._redraw(); | |||||
| }; | |||||
| /** | |||||
| * create the toolbar to edit edges | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._createEditEdgeToolbar = function() { | |||||
| // clear the toolbar | |||||
| this._clearManipulatorBar(); | |||||
| if (this.boundFunction) { | |||||
| this.off('select', this.boundFunction); | |||||
| } | |||||
| this.edgeBeingEdited = this._getSelectedEdge(); | |||||
| this.edgeBeingEdited._enableControlNodes(); | |||||
| this.manipulationDiv.innerHTML = "" + | |||||
| "<span class='network-manipulationUI back' id='network-manipulate-back'>" + | |||||
| "<span class='network-manipulationLabel'>" + this.constants.labels['back'] + " </span></span>" + | |||||
| "<div class='network-seperatorLine'></div>" + | |||||
| "<span class='network-manipulationUI none' id='network-manipulate-back'>" + | |||||
| "<span id='network-manipulatorLabel' class='network-manipulationLabel'>" + this.constants.labels['editEdgeDescription'] + "</span></span>"; | |||||
| // bind the icon | |||||
| var backButton = document.getElementById("network-manipulate-back"); | |||||
| backButton.onclick = this._createManipulatorBar.bind(this); | |||||
| // temporarily overload functions | |||||
| this.cachedFunctions["_handleTouch"] = this._handleTouch; | |||||
| this.cachedFunctions["_handleOnRelease"] = this._handleOnRelease; | |||||
| this.cachedFunctions["_handleTap"] = this._handleTap; | |||||
| this.cachedFunctions["_handleDragStart"] = this._handleDragStart; | |||||
| this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag; | |||||
| this._handleTouch = this._selectControlNode; | |||||
| this._handleTap = function () {}; | |||||
| this._handleOnDrag = this._controlNodeDrag; | |||||
| this._handleDragStart = function () {} | |||||
| this._handleOnRelease = this._releaseControlNode; | |||||
| // redraw to show the unselect | |||||
| this._redraw(); | |||||
| }; | |||||
| /** | |||||
| * the function bound to the selection event. It checks if you want to connect a cluster and changes the description | |||||
| * to walk the user through the process. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._selectControlNode = function(pointer) { | |||||
| this.edgeBeingEdited.controlNodes.from.unselect(); | |||||
| this.edgeBeingEdited.controlNodes.to.unselect(); | |||||
| this.selectedControlNode = this.edgeBeingEdited._getSelectedControlNode(this._XconvertDOMtoCanvas(pointer.x),this._YconvertDOMtoCanvas(pointer.y)); | |||||
| if (this.selectedControlNode !== null) { | |||||
| this.selectedControlNode.select(); | |||||
| this.freezeSimulation = true; | |||||
| } | |||||
| this._redraw(); | |||||
| }; | |||||
| /** | |||||
| * the function bound to the selection event. It checks if you want to connect a cluster and changes the description | |||||
| * to walk the user through the process. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._controlNodeDrag = function(event) { | |||||
| var pointer = this._getPointer(event.gesture.center); | |||||
| if (this.selectedControlNode !== null && this.selectedControlNode !== undefined) { | |||||
| this.selectedControlNode.x = this._XconvertDOMtoCanvas(pointer.x); | |||||
| this.selectedControlNode.y = this._YconvertDOMtoCanvas(pointer.y); | |||||
| } | |||||
| this._redraw(); | |||||
| }; | |||||
| exports._releaseControlNode = function(pointer) { | |||||
| var newNode = this._getNodeAt(pointer); | |||||
| if (newNode != null) { | |||||
| if (this.edgeBeingEdited.controlNodes.from.selected == true) { | |||||
| this._editEdge(newNode.id, this.edgeBeingEdited.to.id); | |||||
| this.edgeBeingEdited.controlNodes.from.unselect(); | |||||
| } | |||||
| if (this.edgeBeingEdited.controlNodes.to.selected == true) { | |||||
| this._editEdge(this.edgeBeingEdited.from.id, newNode.id); | |||||
| this.edgeBeingEdited.controlNodes.to.unselect(); | |||||
| } | |||||
| } | |||||
| else { | |||||
| this.edgeBeingEdited._restoreControlNodes(); | |||||
| } | |||||
| this.freezeSimulation = false; | |||||
| this._redraw(); | |||||
| }; | |||||
| /** | |||||
| * the function bound to the selection event. It checks if you want to connect a cluster and changes the description | |||||
| * to walk the user through the process. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._handleConnect = function(pointer) { | |||||
| if (this._getSelectedNodeCount() == 0) { | |||||
| var node = this._getNodeAt(pointer); | |||||
| if (node != null) { | |||||
| if (node.clusterSize > 1) { | |||||
| alert("Cannot create edges to a cluster.") | |||||
| } | |||||
| else { | |||||
| this._selectObject(node,false); | |||||
| // create a node the temporary line can look at | |||||
| this.sectors['support']['nodes']['targetNode'] = new Node({id:'targetNode'},{},{},this.constants); | |||||
| this.sectors['support']['nodes']['targetNode'].x = node.x; | |||||
| this.sectors['support']['nodes']['targetNode'].y = node.y; | |||||
| this.sectors['support']['nodes']['targetViaNode'] = new Node({id:'targetViaNode'},{},{},this.constants); | |||||
| this.sectors['support']['nodes']['targetViaNode'].x = node.x; | |||||
| this.sectors['support']['nodes']['targetViaNode'].y = node.y; | |||||
| this.sectors['support']['nodes']['targetViaNode'].parentEdgeId = "connectionEdge"; | |||||
| // create a temporary edge | |||||
| this.edges['connectionEdge'] = new Edge({id:"connectionEdge",from:node.id,to:this.sectors['support']['nodes']['targetNode'].id}, this, this.constants); | |||||
| this.edges['connectionEdge'].from = node; | |||||
| this.edges['connectionEdge'].connected = true; | |||||
| this.edges['connectionEdge'].smooth = true; | |||||
| this.edges['connectionEdge'].selected = true; | |||||
| this.edges['connectionEdge'].to = this.sectors['support']['nodes']['targetNode']; | |||||
| this.edges['connectionEdge'].via = this.sectors['support']['nodes']['targetViaNode']; | |||||
| this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag; | |||||
| this._handleOnDrag = function(event) { | |||||
| var pointer = this._getPointer(event.gesture.center); | |||||
| this.sectors['support']['nodes']['targetNode'].x = this._XconvertDOMtoCanvas(pointer.x); | |||||
| this.sectors['support']['nodes']['targetNode'].y = this._YconvertDOMtoCanvas(pointer.y); | |||||
| this.sectors['support']['nodes']['targetViaNode'].x = 0.5 * (this._XconvertDOMtoCanvas(pointer.x) + this.edges['connectionEdge'].from.x); | |||||
| this.sectors['support']['nodes']['targetViaNode'].y = this._YconvertDOMtoCanvas(pointer.y); | |||||
| }; | |||||
| this.moving = true; | |||||
| this.start(); | |||||
| } | |||||
| } | |||||
| } | |||||
| }; | |||||
| exports._finishConnect = function(pointer) { | |||||
| if (this._getSelectedNodeCount() == 1) { | |||||
| // restore the drag function | |||||
| this._handleOnDrag = this.cachedFunctions["_handleOnDrag"]; | |||||
| delete this.cachedFunctions["_handleOnDrag"]; | |||||
| // remember the edge id | |||||
| var connectFromId = this.edges['connectionEdge'].fromId; | |||||
| // remove the temporary nodes and edge | |||||
| delete this.edges['connectionEdge']; | |||||
| delete this.sectors['support']['nodes']['targetNode']; | |||||
| delete this.sectors['support']['nodes']['targetViaNode']; | |||||
| var node = this._getNodeAt(pointer); | |||||
| if (node != null) { | |||||
| if (node.clusterSize > 1) { | |||||
| alert("Cannot create edges to a cluster.") | |||||
| } | |||||
| else { | |||||
| this._createEdge(connectFromId,node.id); | |||||
| this._createManipulatorBar(); | |||||
| } | |||||
| } | |||||
| this._unselectAll(); | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Adds a node on the specified location | |||||
| */ | |||||
| exports._addNode = function() { | |||||
| if (this._selectionIsEmpty() && this.editMode == true) { | |||||
| var positionObject = this._pointerToPositionObject(this.pointerPosition); | |||||
| var defaultData = {id:util.randomUUID(),x:positionObject.left,y:positionObject.top,label:"new",allowedToMoveX:true,allowedToMoveY:true}; | |||||
| if (this.triggerFunctions.add) { | |||||
| if (this.triggerFunctions.add.length == 2) { | |||||
| var me = this; | |||||
| this.triggerFunctions.add(defaultData, function(finalizedData) { | |||||
| me.nodesData.add(finalizedData); | |||||
| me._createManipulatorBar(); | |||||
| me.moving = true; | |||||
| me.start(); | |||||
| }); | |||||
| } | |||||
| else { | |||||
| alert(this.constants.labels['addError']); | |||||
| this._createManipulatorBar(); | |||||
| this.moving = true; | |||||
| this.start(); | |||||
| } | |||||
| } | |||||
| else { | |||||
| this.nodesData.add(defaultData); | |||||
| this._createManipulatorBar(); | |||||
| this.moving = true; | |||||
| this.start(); | |||||
| } | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * connect two nodes with a new edge. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._createEdge = function(sourceNodeId,targetNodeId) { | |||||
| if (this.editMode == true) { | |||||
| var defaultData = {from:sourceNodeId, to:targetNodeId}; | |||||
| if (this.triggerFunctions.connect) { | |||||
| if (this.triggerFunctions.connect.length == 2) { | |||||
| var me = this; | |||||
| this.triggerFunctions.connect(defaultData, function(finalizedData) { | |||||
| me.edgesData.add(finalizedData); | |||||
| me.moving = true; | |||||
| me.start(); | |||||
| }); | |||||
| } | |||||
| else { | |||||
| alert(this.constants.labels["linkError"]); | |||||
| this.moving = true; | |||||
| this.start(); | |||||
| } | |||||
| } | |||||
| else { | |||||
| this.edgesData.add(defaultData); | |||||
| this.moving = true; | |||||
| this.start(); | |||||
| } | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * connect two nodes with a new edge. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._editEdge = function(sourceNodeId,targetNodeId) { | |||||
| if (this.editMode == true) { | |||||
| var defaultData = {id: this.edgeBeingEdited.id, from:sourceNodeId, to:targetNodeId}; | |||||
| if (this.triggerFunctions.editEdge) { | |||||
| if (this.triggerFunctions.editEdge.length == 2) { | |||||
| var me = this; | |||||
| this.triggerFunctions.editEdge(defaultData, function(finalizedData) { | |||||
| me.edgesData.update(finalizedData); | |||||
| me.moving = true; | |||||
| me.start(); | |||||
| }); | |||||
| } | |||||
| else { | |||||
| alert(this.constants.labels["linkError"]); | |||||
| this.moving = true; | |||||
| this.start(); | |||||
| } | |||||
| } | |||||
| else { | |||||
| this.edgesData.update(defaultData); | |||||
| this.moving = true; | |||||
| this.start(); | |||||
| } | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Create the toolbar to edit the selected node. The label and the color can be changed. Other colors are derived from the chosen color. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._editNode = function() { | |||||
| if (this.triggerFunctions.edit && this.editMode == true) { | |||||
| var node = this._getSelectedNode(); | |||||
| var data = {id:node.id, | |||||
| label: node.label, | |||||
| group: node.group, | |||||
| shape: node.shape, | |||||
| color: { | |||||
| background:node.color.background, | |||||
| border:node.color.border, | |||||
| highlight: { | |||||
| background:node.color.highlight.background, | |||||
| border:node.color.highlight.border | |||||
| } | |||||
| }}; | |||||
| if (this.triggerFunctions.edit.length == 2) { | |||||
| var me = this; | |||||
| this.triggerFunctions.edit(data, function (finalizedData) { | |||||
| me.nodesData.update(finalizedData); | |||||
| me._createManipulatorBar(); | |||||
| me.moving = true; | |||||
| me.start(); | |||||
| }); | |||||
| } | |||||
| else { | |||||
| alert(this.constants.labels["editError"]); | |||||
| } | |||||
| } | |||||
| else { | |||||
| alert(this.constants.labels["editBoundError"]); | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * delete everything in the selection | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._deleteSelected = function() { | |||||
| if (!this._selectionIsEmpty() && this.editMode == true) { | |||||
| if (!this._clusterInSelection()) { | |||||
| var selectedNodes = this.getSelectedNodes(); | |||||
| var selectedEdges = this.getSelectedEdges(); | |||||
| if (this.triggerFunctions.del) { | |||||
| var me = this; | |||||
| var data = {nodes: selectedNodes, edges: selectedEdges}; | |||||
| if (this.triggerFunctions.del.length = 2) { | |||||
| this.triggerFunctions.del(data, function (finalizedData) { | |||||
| me.edgesData.remove(finalizedData.edges); | |||||
| me.nodesData.remove(finalizedData.nodes); | |||||
| me._unselectAll(); | |||||
| me.moving = true; | |||||
| me.start(); | |||||
| }); | |||||
| } | |||||
| else { | |||||
| alert(this.constants.labels["deleteError"]) | |||||
| } | |||||
| } | |||||
| else { | |||||
| this.edgesData.remove(selectedEdges); | |||||
| this.nodesData.remove(selectedNodes); | |||||
| this._unselectAll(); | |||||
| this.moving = true; | |||||
| this.start(); | |||||
| } | |||||
| } | |||||
| else { | |||||
| alert(this.constants.labels["deleteClusterError"]); | |||||
| } | |||||
| } | |||||
| }; | |||||
| @ -0,0 +1,198 @@ | |||||
| var PhysicsMixin = require('./physics/PhysicsMixin'); | |||||
| var ClusterMixin = require('./ClusterMixin'); | |||||
| var SectorsMixin = require('./SectorsMixin'); | |||||
| var SelectionMixin = require('./SelectionMixin'); | |||||
| var ManipulationMixin = require('./ManipulationMixin'); | |||||
| var NavigationMixin = require('./NavigationMixin'); | |||||
| var HierarchicalLayoutMixin = require('./HierarchicalLayoutMixin'); | |||||
| /** | |||||
| * Load a mixin into the network object | |||||
| * | |||||
| * @param {Object} sourceVariable | this object has to contain functions. | |||||
| * @private | |||||
| */ | |||||
| exports._loadMixin = function (sourceVariable) { | |||||
| for (var mixinFunction in sourceVariable) { | |||||
| if (sourceVariable.hasOwnProperty(mixinFunction)) { | |||||
| this[mixinFunction] = sourceVariable[mixinFunction]; | |||||
| } | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * removes a mixin from the network object. | |||||
| * | |||||
| * @param {Object} sourceVariable | this object has to contain functions. | |||||
| * @private | |||||
| */ | |||||
| exports._clearMixin = function (sourceVariable) { | |||||
| for (var mixinFunction in sourceVariable) { | |||||
| if (sourceVariable.hasOwnProperty(mixinFunction)) { | |||||
| this[mixinFunction] = undefined; | |||||
| } | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Mixin the physics system and initialize the parameters required. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._loadPhysicsSystem = function () { | |||||
| this._loadMixin(PhysicsMixin); | |||||
| this._loadSelectedForceSolver(); | |||||
| if (this.constants.configurePhysics == true) { | |||||
| this._loadPhysicsConfiguration(); | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Mixin the cluster system and initialize the parameters required. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._loadClusterSystem = function () { | |||||
| this.clusterSession = 0; | |||||
| this.hubThreshold = 5; | |||||
| this._loadMixin(ClusterMixin); | |||||
| }; | |||||
| /** | |||||
| * Mixin the sector system and initialize the parameters required | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._loadSectorSystem = function () { | |||||
| this.sectors = {}; | |||||
| this.activeSector = ["default"]; | |||||
| this.sectors["active"] = {}; | |||||
| this.sectors["active"]["default"] = {"nodes": {}, | |||||
| "edges": {}, | |||||
| "nodeIndices": [], | |||||
| "formationScale": 1.0, | |||||
| "drawingNode": undefined }; | |||||
| this.sectors["frozen"] = {}; | |||||
| this.sectors["support"] = {"nodes": {}, | |||||
| "edges": {}, | |||||
| "nodeIndices": [], | |||||
| "formationScale": 1.0, | |||||
| "drawingNode": undefined }; | |||||
| this.nodeIndices = this.sectors["active"]["default"]["nodeIndices"]; // the node indices list is used to speed up the computation of the repulsion fields | |||||
| this._loadMixin(SectorsMixin); | |||||
| }; | |||||
| /** | |||||
| * Mixin the selection system and initialize the parameters required | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._loadSelectionSystem = function () { | |||||
| this.selectionObj = {nodes: {}, edges: {}}; | |||||
| this._loadMixin(SelectionMixin); | |||||
| }; | |||||
| /** | |||||
| * Mixin the navigationUI (User Interface) system and initialize the parameters required | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._loadManipulationSystem = function () { | |||||
| // reset global variables -- these are used by the selection of nodes and edges. | |||||
| this.blockConnectingEdgeSelection = false; | |||||
| this.forceAppendSelection = false; | |||||
| if (this.constants.dataManipulation.enabled == true) { | |||||
| // load the manipulator HTML elements. All styling done in css. | |||||
| if (this.manipulationDiv === undefined) { | |||||
| this.manipulationDiv = document.createElement('div'); | |||||
| this.manipulationDiv.className = 'network-manipulationDiv'; | |||||
| this.manipulationDiv.id = 'network-manipulationDiv'; | |||||
| if (this.editMode == true) { | |||||
| this.manipulationDiv.style.display = "block"; | |||||
| } | |||||
| else { | |||||
| this.manipulationDiv.style.display = "none"; | |||||
| } | |||||
| this.containerElement.insertBefore(this.manipulationDiv, this.frame); | |||||
| } | |||||
| if (this.editModeDiv === undefined) { | |||||
| this.editModeDiv = document.createElement('div'); | |||||
| this.editModeDiv.className = 'network-manipulation-editMode'; | |||||
| this.editModeDiv.id = 'network-manipulation-editMode'; | |||||
| if (this.editMode == true) { | |||||
| this.editModeDiv.style.display = "none"; | |||||
| } | |||||
| else { | |||||
| this.editModeDiv.style.display = "block"; | |||||
| } | |||||
| this.containerElement.insertBefore(this.editModeDiv, this.frame); | |||||
| } | |||||
| if (this.closeDiv === undefined) { | |||||
| this.closeDiv = document.createElement('div'); | |||||
| this.closeDiv.className = 'network-manipulation-closeDiv'; | |||||
| this.closeDiv.id = 'network-manipulation-closeDiv'; | |||||
| this.closeDiv.style.display = this.manipulationDiv.style.display; | |||||
| this.containerElement.insertBefore(this.closeDiv, this.frame); | |||||
| } | |||||
| // load the manipulation functions | |||||
| this._loadMixin(ManipulationMixin); | |||||
| // create the manipulator toolbar | |||||
| this._createManipulatorBar(); | |||||
| } | |||||
| else { | |||||
| if (this.manipulationDiv !== undefined) { | |||||
| // removes all the bindings and overloads | |||||
| this._createManipulatorBar(); | |||||
| // remove the manipulation divs | |||||
| this.containerElement.removeChild(this.manipulationDiv); | |||||
| this.containerElement.removeChild(this.editModeDiv); | |||||
| this.containerElement.removeChild(this.closeDiv); | |||||
| this.manipulationDiv = undefined; | |||||
| this.editModeDiv = undefined; | |||||
| this.closeDiv = undefined; | |||||
| // remove the mixin functions | |||||
| this._clearMixin(ManipulationMixin); | |||||
| } | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Mixin the navigation (User Interface) system and initialize the parameters required | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._loadNavigationControls = function () { | |||||
| this._loadMixin(NavigationMixin); | |||||
| // the clean function removes the button divs, this is done to remove the bindings. | |||||
| this._cleanNavigation(); | |||||
| if (this.constants.navigation.enabled == true) { | |||||
| this._loadNavigationElements(); | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Mixin the hierarchical layout system. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._loadHierarchySystem = function () { | |||||
| this._loadMixin(HierarchicalLayoutMixin); | |||||
| }; | |||||
| @ -0,0 +1,196 @@ | |||||
| exports._cleanNavigation = function() { | |||||
| // clean up previous navigation items | |||||
| var wrapper = document.getElementById('network-navigation_wrapper'); | |||||
| if (wrapper != null) { | |||||
| this.containerElement.removeChild(wrapper); | |||||
| } | |||||
| document.onmouseup = null; | |||||
| }; | |||||
| /** | |||||
| * Creation of the navigation controls nodes. They are drawn over the rest of the nodes and are not affected by scale and translation | |||||
| * they have a triggerFunction which is called on click. If the position of the navigation controls is dependent | |||||
| * on this.frame.canvas.clientWidth or this.frame.canvas.clientHeight, we flag horizontalAlignLeft and verticalAlignTop false. | |||||
| * This means that the location will be corrected by the _relocateNavigation function on a size change of the canvas. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._loadNavigationElements = function() { | |||||
| this._cleanNavigation(); | |||||
| this.navigationDivs = {}; | |||||
| var navigationDivs = ['up','down','left','right','zoomIn','zoomOut','zoomExtends']; | |||||
| var navigationDivActions = ['_moveUp','_moveDown','_moveLeft','_moveRight','_zoomIn','_zoomOut','zoomExtent']; | |||||
| this.navigationDivs['wrapper'] = document.createElement('div'); | |||||
| this.navigationDivs['wrapper'].id = "network-navigation_wrapper"; | |||||
| this.navigationDivs['wrapper'].style.position = "absolute"; | |||||
| this.navigationDivs['wrapper'].style.width = this.frame.canvas.clientWidth + "px"; | |||||
| this.navigationDivs['wrapper'].style.height = this.frame.canvas.clientHeight + "px"; | |||||
| this.containerElement.insertBefore(this.navigationDivs['wrapper'],this.frame); | |||||
| for (var i = 0; i < navigationDivs.length; i++) { | |||||
| this.navigationDivs[navigationDivs[i]] = document.createElement('div'); | |||||
| this.navigationDivs[navigationDivs[i]].id = "network-navigation_" + navigationDivs[i]; | |||||
| this.navigationDivs[navigationDivs[i]].className = "network-navigation " + navigationDivs[i]; | |||||
| this.navigationDivs['wrapper'].appendChild(this.navigationDivs[navigationDivs[i]]); | |||||
| this.navigationDivs[navigationDivs[i]].onmousedown = this[navigationDivActions[i]].bind(this); | |||||
| } | |||||
| document.onmouseup = this._stopMovement.bind(this); | |||||
| }; | |||||
| /** | |||||
| * this stops all movement induced by the navigation buttons | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._stopMovement = function() { | |||||
| this._xStopMoving(); | |||||
| this._yStopMoving(); | |||||
| this._stopZoom(); | |||||
| }; | |||||
| /** | |||||
| * stops the actions performed by page up and down etc. | |||||
| * | |||||
| * @param event | |||||
| * @private | |||||
| */ | |||||
| exports._preventDefault = function(event) { | |||||
| if (event !== undefined) { | |||||
| if (event.preventDefault) { | |||||
| event.preventDefault(); | |||||
| } else { | |||||
| event.returnValue = false; | |||||
| } | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * move the screen up | |||||
| * By using the increments, instead of adding a fixed number to the translation, we keep fluent and | |||||
| * instant movement. The onKeypress event triggers immediately, then pauses, then triggers frequently | |||||
| * To avoid this behaviour, we do the translation in the start loop. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._moveUp = function(event) { | |||||
| this.yIncrement = this.constants.keyboard.speed.y; | |||||
| this.start(); // if there is no node movement, the calculation wont be done | |||||
| this._preventDefault(event); | |||||
| if (this.navigationDivs) { | |||||
| this.navigationDivs['up'].className += " active"; | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * move the screen down | |||||
| * @private | |||||
| */ | |||||
| exports._moveDown = function(event) { | |||||
| this.yIncrement = -this.constants.keyboard.speed.y; | |||||
| this.start(); // if there is no node movement, the calculation wont be done | |||||
| this._preventDefault(event); | |||||
| if (this.navigationDivs) { | |||||
| this.navigationDivs['down'].className += " active"; | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * move the screen left | |||||
| * @private | |||||
| */ | |||||
| exports._moveLeft = function(event) { | |||||
| this.xIncrement = this.constants.keyboard.speed.x; | |||||
| this.start(); // if there is no node movement, the calculation wont be done | |||||
| this._preventDefault(event); | |||||
| if (this.navigationDivs) { | |||||
| this.navigationDivs['left'].className += " active"; | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * move the screen right | |||||
| * @private | |||||
| */ | |||||
| exports._moveRight = function(event) { | |||||
| this.xIncrement = -this.constants.keyboard.speed.y; | |||||
| this.start(); // if there is no node movement, the calculation wont be done | |||||
| this._preventDefault(event); | |||||
| if (this.navigationDivs) { | |||||
| this.navigationDivs['right'].className += " active"; | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Zoom in, using the same method as the movement. | |||||
| * @private | |||||
| */ | |||||
| exports._zoomIn = function(event) { | |||||
| this.zoomIncrement = this.constants.keyboard.speed.zoom; | |||||
| this.start(); // if there is no node movement, the calculation wont be done | |||||
| this._preventDefault(event); | |||||
| if (this.navigationDivs) { | |||||
| this.navigationDivs['zoomIn'].className += " active"; | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Zoom out | |||||
| * @private | |||||
| */ | |||||
| exports._zoomOut = function() { | |||||
| this.zoomIncrement = -this.constants.keyboard.speed.zoom; | |||||
| this.start(); // if there is no node movement, the calculation wont be done | |||||
| this._preventDefault(event); | |||||
| if (this.navigationDivs) { | |||||
| this.navigationDivs['zoomOut'].className += " active"; | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Stop zooming and unhighlight the zoom controls | |||||
| * @private | |||||
| */ | |||||
| exports._stopZoom = function() { | |||||
| this.zoomIncrement = 0; | |||||
| if (this.navigationDivs) { | |||||
| this.navigationDivs['zoomIn'].className = this.navigationDivs['zoomIn'].className.replace(" active",""); | |||||
| this.navigationDivs['zoomOut'].className = this.navigationDivs['zoomOut'].className.replace(" active",""); | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Stop moving in the Y direction and unHighlight the up and down | |||||
| * @private | |||||
| */ | |||||
| exports._yStopMoving = function() { | |||||
| this.yIncrement = 0; | |||||
| if (this.navigationDivs) { | |||||
| this.navigationDivs['up'].className = this.navigationDivs['up'].className.replace(" active",""); | |||||
| this.navigationDivs['down'].className = this.navigationDivs['down'].className.replace(" active",""); | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Stop moving in the X direction and unHighlight left and right. | |||||
| * @private | |||||
| */ | |||||
| exports._xStopMoving = function() { | |||||
| this.xIncrement = 0; | |||||
| if (this.navigationDivs) { | |||||
| this.navigationDivs['left'].className = this.navigationDivs['left'].className.replace(" active",""); | |||||
| this.navigationDivs['right'].className = this.navigationDivs['right'].className.replace(" active",""); | |||||
| } | |||||
| }; | |||||
| @ -0,0 +1,548 @@ | |||||
| var util = require('../../util'); | |||||
| /** | |||||
| * Creation of the SectorMixin var. | |||||
| * | |||||
| * This contains all the functions the Network object can use to employ the sector system. | |||||
| * The sector system is always used by Network, though the benefits only apply to the use of clustering. | |||||
| * If clustering is not used, there is no overhead except for a duplicate object with references to nodes and edges. | |||||
| */ | |||||
| /** | |||||
| * This function is only called by the setData function of the Network object. | |||||
| * This loads the global references into the active sector. This initializes the sector. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._putDataInSector = function() { | |||||
| this.sectors["active"][this._sector()].nodes = this.nodes; | |||||
| this.sectors["active"][this._sector()].edges = this.edges; | |||||
| this.sectors["active"][this._sector()].nodeIndices = this.nodeIndices; | |||||
| }; | |||||
| /** | |||||
| * /** | |||||
| * This function sets the global references to nodes, edges and nodeIndices back to | |||||
| * those of the supplied (active) sector. If a type is defined, do the specific type | |||||
| * | |||||
| * @param {String} sectorId | |||||
| * @param {String} [sectorType] | "active" or "frozen" | |||||
| * @private | |||||
| */ | |||||
| exports._switchToSector = function(sectorId, sectorType) { | |||||
| if (sectorType === undefined || sectorType == "active") { | |||||
| this._switchToActiveSector(sectorId); | |||||
| } | |||||
| else { | |||||
| this._switchToFrozenSector(sectorId); | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * This function sets the global references to nodes, edges and nodeIndices back to | |||||
| * those of the supplied active sector. | |||||
| * | |||||
| * @param sectorId | |||||
| * @private | |||||
| */ | |||||
| exports._switchToActiveSector = function(sectorId) { | |||||
| this.nodeIndices = this.sectors["active"][sectorId]["nodeIndices"]; | |||||
| this.nodes = this.sectors["active"][sectorId]["nodes"]; | |||||
| this.edges = this.sectors["active"][sectorId]["edges"]; | |||||
| }; | |||||
| /** | |||||
| * This function sets the global references to nodes, edges and nodeIndices back to | |||||
| * those of the supplied active sector. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._switchToSupportSector = function() { | |||||
| this.nodeIndices = this.sectors["support"]["nodeIndices"]; | |||||
| this.nodes = this.sectors["support"]["nodes"]; | |||||
| this.edges = this.sectors["support"]["edges"]; | |||||
| }; | |||||
| /** | |||||
| * This function sets the global references to nodes, edges and nodeIndices back to | |||||
| * those of the supplied frozen sector. | |||||
| * | |||||
| * @param sectorId | |||||
| * @private | |||||
| */ | |||||
| exports._switchToFrozenSector = function(sectorId) { | |||||
| this.nodeIndices = this.sectors["frozen"][sectorId]["nodeIndices"]; | |||||
| this.nodes = this.sectors["frozen"][sectorId]["nodes"]; | |||||
| this.edges = this.sectors["frozen"][sectorId]["edges"]; | |||||
| }; | |||||
| /** | |||||
| * This function sets the global references to nodes, edges and nodeIndices back to | |||||
| * those of the currently active sector. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._loadLatestSector = function() { | |||||
| this._switchToSector(this._sector()); | |||||
| }; | |||||
| /** | |||||
| * This function returns the currently active sector Id | |||||
| * | |||||
| * @returns {String} | |||||
| * @private | |||||
| */ | |||||
| exports._sector = function() { | |||||
| return this.activeSector[this.activeSector.length-1]; | |||||
| }; | |||||
| /** | |||||
| * This function returns the previously active sector Id | |||||
| * | |||||
| * @returns {String} | |||||
| * @private | |||||
| */ | |||||
| exports._previousSector = function() { | |||||
| if (this.activeSector.length > 1) { | |||||
| return this.activeSector[this.activeSector.length-2]; | |||||
| } | |||||
| else { | |||||
| throw new TypeError('there are not enough sectors in the this.activeSector array.'); | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * We add the active sector at the end of the this.activeSector array | |||||
| * This ensures it is the currently active sector returned by _sector() and it reaches the top | |||||
| * of the activeSector stack. When we reverse our steps we move from the end to the beginning of this stack. | |||||
| * | |||||
| * @param newId | |||||
| * @private | |||||
| */ | |||||
| exports._setActiveSector = function(newId) { | |||||
| this.activeSector.push(newId); | |||||
| }; | |||||
| /** | |||||
| * We remove the currently active sector id from the active sector stack. This happens when | |||||
| * we reactivate the previously active sector | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._forgetLastSector = function() { | |||||
| this.activeSector.pop(); | |||||
| }; | |||||
| /** | |||||
| * This function creates a new active sector with the supplied newId. This newId | |||||
| * is the expanding node id. | |||||
| * | |||||
| * @param {String} newId | Id of the new active sector | |||||
| * @private | |||||
| */ | |||||
| exports._createNewSector = function(newId) { | |||||
| // create the new sector | |||||
| this.sectors["active"][newId] = {"nodes":{}, | |||||
| "edges":{}, | |||||
| "nodeIndices":[], | |||||
| "formationScale": this.scale, | |||||
| "drawingNode": undefined}; | |||||
| // create the new sector render node. This gives visual feedback that you are in a new sector. | |||||
| this.sectors["active"][newId]['drawingNode'] = new Node( | |||||
| {id:newId, | |||||
| color: { | |||||
| background: "#eaefef", | |||||
| border: "495c5e" | |||||
| } | |||||
| },{},{},this.constants); | |||||
| this.sectors["active"][newId]['drawingNode'].clusterSize = 2; | |||||
| }; | |||||
| /** | |||||
| * This function removes the currently active sector. This is called when we create a new | |||||
| * active sector. | |||||
| * | |||||
| * @param {String} sectorId | Id of the active sector that will be removed | |||||
| * @private | |||||
| */ | |||||
| exports._deleteActiveSector = function(sectorId) { | |||||
| delete this.sectors["active"][sectorId]; | |||||
| }; | |||||
| /** | |||||
| * This function removes the currently active sector. This is called when we reactivate | |||||
| * the previously active sector. | |||||
| * | |||||
| * @param {String} sectorId | Id of the active sector that will be removed | |||||
| * @private | |||||
| */ | |||||
| exports._deleteFrozenSector = function(sectorId) { | |||||
| delete this.sectors["frozen"][sectorId]; | |||||
| }; | |||||
| /** | |||||
| * Freezing an active sector means moving it from the "active" object to the "frozen" object. | |||||
| * We copy the references, then delete the active entree. | |||||
| * | |||||
| * @param sectorId | |||||
| * @private | |||||
| */ | |||||
| exports._freezeSector = function(sectorId) { | |||||
| // we move the set references from the active to the frozen stack. | |||||
| this.sectors["frozen"][sectorId] = this.sectors["active"][sectorId]; | |||||
| // we have moved the sector data into the frozen set, we now remove it from the active set | |||||
| this._deleteActiveSector(sectorId); | |||||
| }; | |||||
| /** | |||||
| * This is the reverse operation of _freezeSector. Activating means moving the sector from the "frozen" | |||||
| * object to the "active" object. | |||||
| * | |||||
| * @param sectorId | |||||
| * @private | |||||
| */ | |||||
| exports._activateSector = function(sectorId) { | |||||
| // we move the set references from the frozen to the active stack. | |||||
| this.sectors["active"][sectorId] = this.sectors["frozen"][sectorId]; | |||||
| // we have moved the sector data into the active set, we now remove it from the frozen stack | |||||
| this._deleteFrozenSector(sectorId); | |||||
| }; | |||||
| /** | |||||
| * This function merges the data from the currently active sector with a frozen sector. This is used | |||||
| * in the process of reverting back to the previously active sector. | |||||
| * The data that is placed in the frozen (the previously active) sector is the node that has been removed from it | |||||
| * upon the creation of a new active sector. | |||||
| * | |||||
| * @param sectorId | |||||
| * @private | |||||
| */ | |||||
| exports._mergeThisWithFrozen = function(sectorId) { | |||||
| // copy all nodes | |||||
| for (var nodeId in this.nodes) { | |||||
| if (this.nodes.hasOwnProperty(nodeId)) { | |||||
| this.sectors["frozen"][sectorId]["nodes"][nodeId] = this.nodes[nodeId]; | |||||
| } | |||||
| } | |||||
| // copy all edges (if not fully clustered, else there are no edges) | |||||
| for (var edgeId in this.edges) { | |||||
| if (this.edges.hasOwnProperty(edgeId)) { | |||||
| this.sectors["frozen"][sectorId]["edges"][edgeId] = this.edges[edgeId]; | |||||
| } | |||||
| } | |||||
| // merge the nodeIndices | |||||
| for (var i = 0; i < this.nodeIndices.length; i++) { | |||||
| this.sectors["frozen"][sectorId]["nodeIndices"].push(this.nodeIndices[i]); | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * This clusters the sector to one cluster. It was a single cluster before this process started so | |||||
| * we revert to that state. The clusterToFit function with a maximum size of 1 node does this. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._collapseThisToSingleCluster = function() { | |||||
| this.clusterToFit(1,false); | |||||
| }; | |||||
| /** | |||||
| * We create a new active sector from the node that we want to open. | |||||
| * | |||||
| * @param node | |||||
| * @private | |||||
| */ | |||||
| exports._addSector = function(node) { | |||||
| // this is the currently active sector | |||||
| var sector = this._sector(); | |||||
| // // this should allow me to select nodes from a frozen set. | |||||
| // if (this.sectors['active'][sector]["nodes"].hasOwnProperty(node.id)) { | |||||
| // console.log("the node is part of the active sector"); | |||||
| // } | |||||
| // else { | |||||
| // console.log("I dont know what the fuck happened!!"); | |||||
| // } | |||||
| // when we switch to a new sector, we remove the node that will be expanded from the current nodes list. | |||||
| delete this.nodes[node.id]; | |||||
| var unqiueIdentifier = util.randomUUID(); | |||||
| // we fully freeze the currently active sector | |||||
| this._freezeSector(sector); | |||||
| // we create a new active sector. This sector has the Id of the node to ensure uniqueness | |||||
| this._createNewSector(unqiueIdentifier); | |||||
| // we add the active sector to the sectors array to be able to revert these steps later on | |||||
| this._setActiveSector(unqiueIdentifier); | |||||
| // we redirect the global references to the new sector's references. this._sector() now returns unqiueIdentifier | |||||
| this._switchToSector(this._sector()); | |||||
| // finally we add the node we removed from our previous active sector to the new active sector | |||||
| this.nodes[node.id] = node; | |||||
| }; | |||||
| /** | |||||
| * We close the sector that is currently open and revert back to the one before. | |||||
| * If the active sector is the "default" sector, nothing happens. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._collapseSector = function() { | |||||
| // the currently active sector | |||||
| var sector = this._sector(); | |||||
| // we cannot collapse the default sector | |||||
| if (sector != "default") { | |||||
| if ((this.nodeIndices.length == 1) || | |||||
| (this.sectors["active"][sector]["drawingNode"].width*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) || | |||||
| (this.sectors["active"][sector]["drawingNode"].height*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) { | |||||
| var previousSector = this._previousSector(); | |||||
| // we collapse the sector back to a single cluster | |||||
| this._collapseThisToSingleCluster(); | |||||
| // we move the remaining nodes, edges and nodeIndices to the previous sector. | |||||
| // This previous sector is the one we will reactivate | |||||
| this._mergeThisWithFrozen(previousSector); | |||||
| // the previously active (frozen) sector now has all the data from the currently active sector. | |||||
| // we can now delete the active sector. | |||||
| this._deleteActiveSector(sector); | |||||
| // we activate the previously active (and currently frozen) sector. | |||||
| this._activateSector(previousSector); | |||||
| // we load the references from the newly active sector into the global references | |||||
| this._switchToSector(previousSector); | |||||
| // we forget the previously active sector because we reverted to the one before | |||||
| this._forgetLastSector(); | |||||
| // finally, we update the node index list. | |||||
| this._updateNodeIndexList(); | |||||
| // we refresh the list with calulation nodes and calculation node indices. | |||||
| this._updateCalculationNodes(); | |||||
| } | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * This runs a function in all active sectors. This is used in _redraw() and the _initializeForceCalculation(). | |||||
| * | |||||
| * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors | |||||
| * | we dont pass the function itself because then the "this" is the window object | |||||
| * | instead of the Network object | |||||
| * @param {*} [argument] | Optional: arguments to pass to the runFunction | |||||
| * @private | |||||
| */ | |||||
| exports._doInAllActiveSectors = function(runFunction,argument) { | |||||
| if (argument === undefined) { | |||||
| for (var sector in this.sectors["active"]) { | |||||
| if (this.sectors["active"].hasOwnProperty(sector)) { | |||||
| // switch the global references to those of this sector | |||||
| this._switchToActiveSector(sector); | |||||
| this[runFunction](); | |||||
| } | |||||
| } | |||||
| } | |||||
| else { | |||||
| for (var sector in this.sectors["active"]) { | |||||
| if (this.sectors["active"].hasOwnProperty(sector)) { | |||||
| // switch the global references to those of this sector | |||||
| this._switchToActiveSector(sector); | |||||
| var args = Array.prototype.splice.call(arguments, 1); | |||||
| if (args.length > 1) { | |||||
| this[runFunction](args[0],args[1]); | |||||
| } | |||||
| else { | |||||
| this[runFunction](argument); | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| // we revert the global references back to our active sector | |||||
| this._loadLatestSector(); | |||||
| }; | |||||
| /** | |||||
| * This runs a function in all active sectors. This is used in _redraw() and the _initializeForceCalculation(). | |||||
| * | |||||
| * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors | |||||
| * | we dont pass the function itself because then the "this" is the window object | |||||
| * | instead of the Network object | |||||
| * @param {*} [argument] | Optional: arguments to pass to the runFunction | |||||
| * @private | |||||
| */ | |||||
| exports._doInSupportSector = function(runFunction,argument) { | |||||
| if (argument === undefined) { | |||||
| this._switchToSupportSector(); | |||||
| this[runFunction](); | |||||
| } | |||||
| else { | |||||
| this._switchToSupportSector(); | |||||
| var args = Array.prototype.splice.call(arguments, 1); | |||||
| if (args.length > 1) { | |||||
| this[runFunction](args[0],args[1]); | |||||
| } | |||||
| else { | |||||
| this[runFunction](argument); | |||||
| } | |||||
| } | |||||
| // we revert the global references back to our active sector | |||||
| this._loadLatestSector(); | |||||
| }; | |||||
| /** | |||||
| * This runs a function in all frozen sectors. This is used in the _redraw(). | |||||
| * | |||||
| * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors | |||||
| * | we don't pass the function itself because then the "this" is the window object | |||||
| * | instead of the Network object | |||||
| * @param {*} [argument] | Optional: arguments to pass to the runFunction | |||||
| * @private | |||||
| */ | |||||
| exports._doInAllFrozenSectors = function(runFunction,argument) { | |||||
| if (argument === undefined) { | |||||
| for (var sector in this.sectors["frozen"]) { | |||||
| if (this.sectors["frozen"].hasOwnProperty(sector)) { | |||||
| // switch the global references to those of this sector | |||||
| this._switchToFrozenSector(sector); | |||||
| this[runFunction](); | |||||
| } | |||||
| } | |||||
| } | |||||
| else { | |||||
| for (var sector in this.sectors["frozen"]) { | |||||
| if (this.sectors["frozen"].hasOwnProperty(sector)) { | |||||
| // switch the global references to those of this sector | |||||
| this._switchToFrozenSector(sector); | |||||
| var args = Array.prototype.splice.call(arguments, 1); | |||||
| if (args.length > 1) { | |||||
| this[runFunction](args[0],args[1]); | |||||
| } | |||||
| else { | |||||
| this[runFunction](argument); | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| this._loadLatestSector(); | |||||
| }; | |||||
| /** | |||||
| * This runs a function in all sectors. This is used in the _redraw(). | |||||
| * | |||||
| * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors | |||||
| * | we don't pass the function itself because then the "this" is the window object | |||||
| * | instead of the Network object | |||||
| * @param {*} [argument] | Optional: arguments to pass to the runFunction | |||||
| * @private | |||||
| */ | |||||
| exports._doInAllSectors = function(runFunction,argument) { | |||||
| var args = Array.prototype.splice.call(arguments, 1); | |||||
| if (argument === undefined) { | |||||
| this._doInAllActiveSectors(runFunction); | |||||
| this._doInAllFrozenSectors(runFunction); | |||||
| } | |||||
| else { | |||||
| if (args.length > 1) { | |||||
| this._doInAllActiveSectors(runFunction,args[0],args[1]); | |||||
| this._doInAllFrozenSectors(runFunction,args[0],args[1]); | |||||
| } | |||||
| else { | |||||
| this._doInAllActiveSectors(runFunction,argument); | |||||
| this._doInAllFrozenSectors(runFunction,argument); | |||||
| } | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * This clears the nodeIndices list. We cannot use this.nodeIndices = [] because we would break the link with the | |||||
| * active sector. Thus we clear the nodeIndices in the active sector, then reconnect the this.nodeIndices to it. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._clearNodeIndexList = function() { | |||||
| var sector = this._sector(); | |||||
| this.sectors["active"][sector]["nodeIndices"] = []; | |||||
| this.nodeIndices = this.sectors["active"][sector]["nodeIndices"]; | |||||
| }; | |||||
| /** | |||||
| * Draw the encompassing sector node | |||||
| * | |||||
| * @param ctx | |||||
| * @param sectorType | |||||
| * @private | |||||
| */ | |||||
| exports._drawSectorNodes = function(ctx,sectorType) { | |||||
| var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node; | |||||
| for (var sector in this.sectors[sectorType]) { | |||||
| if (this.sectors[sectorType].hasOwnProperty(sector)) { | |||||
| if (this.sectors[sectorType][sector]["drawingNode"] !== undefined) { | |||||
| this._switchToSector(sector,sectorType); | |||||
| minY = 1e9; maxY = -1e9; minX = 1e9; maxX = -1e9; | |||||
| for (var nodeId in this.nodes) { | |||||
| if (this.nodes.hasOwnProperty(nodeId)) { | |||||
| node = this.nodes[nodeId]; | |||||
| node.resize(ctx); | |||||
| if (minX > node.x - 0.5 * node.width) {minX = node.x - 0.5 * node.width;} | |||||
| if (maxX < node.x + 0.5 * node.width) {maxX = node.x + 0.5 * node.width;} | |||||
| if (minY > node.y - 0.5 * node.height) {minY = node.y - 0.5 * node.height;} | |||||
| if (maxY < node.y + 0.5 * node.height) {maxY = node.y + 0.5 * node.height;} | |||||
| } | |||||
| } | |||||
| node = this.sectors[sectorType][sector]["drawingNode"]; | |||||
| node.x = 0.5 * (maxX + minX); | |||||
| node.y = 0.5 * (maxY + minY); | |||||
| node.width = 2 * (node.x - minX); | |||||
| node.height = 2 * (node.y - minY); | |||||
| node.radius = Math.sqrt(Math.pow(0.5*node.width,2) + Math.pow(0.5*node.height,2)); | |||||
| node.setScale(this.scale); | |||||
| node._drawCircle(ctx); | |||||
| } | |||||
| } | |||||
| } | |||||
| }; | |||||
| exports._drawAllSectorNodes = function(ctx) { | |||||
| this._drawSectorNodes(ctx,"frozen"); | |||||
| this._drawSectorNodes(ctx,"active"); | |||||
| this._loadLatestSector(); | |||||
| }; | |||||
| @ -0,0 +1,705 @@ | |||||
| var Node = require('../Node'); | |||||
| /** | |||||
| * This function can be called from the _doInAllSectors function | |||||
| * | |||||
| * @param object | |||||
| * @param overlappingNodes | |||||
| * @private | |||||
| */ | |||||
| exports._getNodesOverlappingWith = function(object, overlappingNodes) { | |||||
| var nodes = this.nodes; | |||||
| for (var nodeId in nodes) { | |||||
| if (nodes.hasOwnProperty(nodeId)) { | |||||
| if (nodes[nodeId].isOverlappingWith(object)) { | |||||
| overlappingNodes.push(nodeId); | |||||
| } | |||||
| } | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * retrieve all nodes overlapping with given object | |||||
| * @param {Object} object An object with parameters left, top, right, bottom | |||||
| * @return {Number[]} An array with id's of the overlapping nodes | |||||
| * @private | |||||
| */ | |||||
| exports._getAllNodesOverlappingWith = function (object) { | |||||
| var overlappingNodes = []; | |||||
| this._doInAllActiveSectors("_getNodesOverlappingWith",object,overlappingNodes); | |||||
| return overlappingNodes; | |||||
| }; | |||||
| /** | |||||
| * Return a position object in canvasspace from a single point in screenspace | |||||
| * | |||||
| * @param pointer | |||||
| * @returns {{left: number, top: number, right: number, bottom: number}} | |||||
| * @private | |||||
| */ | |||||
| exports._pointerToPositionObject = function(pointer) { | |||||
| var x = this._XconvertDOMtoCanvas(pointer.x); | |||||
| var y = this._YconvertDOMtoCanvas(pointer.y); | |||||
| return { | |||||
| left: x, | |||||
| top: y, | |||||
| right: x, | |||||
| bottom: y | |||||
| }; | |||||
| }; | |||||
| /** | |||||
| * Get the top node at the a specific point (like a click) | |||||
| * | |||||
| * @param {{x: Number, y: Number}} pointer | |||||
| * @return {Node | null} node | |||||
| * @private | |||||
| */ | |||||
| exports._getNodeAt = function (pointer) { | |||||
| // we first check if this is an navigation controls element | |||||
| var positionObject = this._pointerToPositionObject(pointer); | |||||
| var overlappingNodes = this._getAllNodesOverlappingWith(positionObject); | |||||
| // if there are overlapping nodes, select the last one, this is the | |||||
| // one which is drawn on top of the others | |||||
| if (overlappingNodes.length > 0) { | |||||
| return this.nodes[overlappingNodes[overlappingNodes.length - 1]]; | |||||
| } | |||||
| else { | |||||
| return null; | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * retrieve all edges overlapping with given object, selector is around center | |||||
| * @param {Object} object An object with parameters left, top, right, bottom | |||||
| * @return {Number[]} An array with id's of the overlapping nodes | |||||
| * @private | |||||
| */ | |||||
| exports._getEdgesOverlappingWith = function (object, overlappingEdges) { | |||||
| var edges = this.edges; | |||||
| for (var edgeId in edges) { | |||||
| if (edges.hasOwnProperty(edgeId)) { | |||||
| if (edges[edgeId].isOverlappingWith(object)) { | |||||
| overlappingEdges.push(edgeId); | |||||
| } | |||||
| } | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * retrieve all nodes overlapping with given object | |||||
| * @param {Object} object An object with parameters left, top, right, bottom | |||||
| * @return {Number[]} An array with id's of the overlapping nodes | |||||
| * @private | |||||
| */ | |||||
| exports._getAllEdgesOverlappingWith = function (object) { | |||||
| var overlappingEdges = []; | |||||
| this._doInAllActiveSectors("_getEdgesOverlappingWith",object,overlappingEdges); | |||||
| return overlappingEdges; | |||||
| }; | |||||
| /** | |||||
| * Place holder. To implement change the _getNodeAt to a _getObjectAt. Have the _getObjectAt call | |||||
| * _getNodeAt and _getEdgesAt, then priortize the selection to user preferences. | |||||
| * | |||||
| * @param pointer | |||||
| * @returns {null} | |||||
| * @private | |||||
| */ | |||||
| exports._getEdgeAt = function(pointer) { | |||||
| var positionObject = this._pointerToPositionObject(pointer); | |||||
| var overlappingEdges = this._getAllEdgesOverlappingWith(positionObject); | |||||
| if (overlappingEdges.length > 0) { | |||||
| return this.edges[overlappingEdges[overlappingEdges.length - 1]]; | |||||
| } | |||||
| else { | |||||
| return null; | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Add object to the selection array. | |||||
| * | |||||
| * @param obj | |||||
| * @private | |||||
| */ | |||||
| exports._addToSelection = function(obj) { | |||||
| if (obj instanceof Node) { | |||||
| this.selectionObj.nodes[obj.id] = obj; | |||||
| } | |||||
| else { | |||||
| this.selectionObj.edges[obj.id] = obj; | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Add object to the selection array. | |||||
| * | |||||
| * @param obj | |||||
| * @private | |||||
| */ | |||||
| exports._addToHover = function(obj) { | |||||
| if (obj instanceof Node) { | |||||
| this.hoverObj.nodes[obj.id] = obj; | |||||
| } | |||||
| else { | |||||
| this.hoverObj.edges[obj.id] = obj; | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Remove a single option from selection. | |||||
| * | |||||
| * @param {Object} obj | |||||
| * @private | |||||
| */ | |||||
| exports._removeFromSelection = function(obj) { | |||||
| if (obj instanceof Node) { | |||||
| delete this.selectionObj.nodes[obj.id]; | |||||
| } | |||||
| else { | |||||
| delete this.selectionObj.edges[obj.id]; | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Unselect all. The selectionObj is useful for this. | |||||
| * | |||||
| * @param {Boolean} [doNotTrigger] | ignore trigger | |||||
| * @private | |||||
| */ | |||||
| exports._unselectAll = function(doNotTrigger) { | |||||
| if (doNotTrigger === undefined) { | |||||
| doNotTrigger = false; | |||||
| } | |||||
| for(var nodeId in this.selectionObj.nodes) { | |||||
| if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { | |||||
| this.selectionObj.nodes[nodeId].unselect(); | |||||
| } | |||||
| } | |||||
| for(var edgeId in this.selectionObj.edges) { | |||||
| if(this.selectionObj.edges.hasOwnProperty(edgeId)) { | |||||
| this.selectionObj.edges[edgeId].unselect(); | |||||
| } | |||||
| } | |||||
| this.selectionObj = {nodes:{},edges:{}}; | |||||
| if (doNotTrigger == false) { | |||||
| this.emit('select', this.getSelection()); | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Unselect all clusters. The selectionObj is useful for this. | |||||
| * | |||||
| * @param {Boolean} [doNotTrigger] | ignore trigger | |||||
| * @private | |||||
| */ | |||||
| exports._unselectClusters = function(doNotTrigger) { | |||||
| if (doNotTrigger === undefined) { | |||||
| doNotTrigger = false; | |||||
| } | |||||
| for (var nodeId in this.selectionObj.nodes) { | |||||
| if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { | |||||
| if (this.selectionObj.nodes[nodeId].clusterSize > 1) { | |||||
| this.selectionObj.nodes[nodeId].unselect(); | |||||
| this._removeFromSelection(this.selectionObj.nodes[nodeId]); | |||||
| } | |||||
| } | |||||
| } | |||||
| if (doNotTrigger == false) { | |||||
| this.emit('select', this.getSelection()); | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * return the number of selected nodes | |||||
| * | |||||
| * @returns {number} | |||||
| * @private | |||||
| */ | |||||
| exports._getSelectedNodeCount = function() { | |||||
| var count = 0; | |||||
| for (var nodeId in this.selectionObj.nodes) { | |||||
| if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { | |||||
| count += 1; | |||||
| } | |||||
| } | |||||
| return count; | |||||
| }; | |||||
| /** | |||||
| * return the selected node | |||||
| * | |||||
| * @returns {number} | |||||
| * @private | |||||
| */ | |||||
| exports._getSelectedNode = function() { | |||||
| for (var nodeId in this.selectionObj.nodes) { | |||||
| if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { | |||||
| return this.selectionObj.nodes[nodeId]; | |||||
| } | |||||
| } | |||||
| return null; | |||||
| }; | |||||
| /** | |||||
| * return the selected edge | |||||
| * | |||||
| * @returns {number} | |||||
| * @private | |||||
| */ | |||||
| exports._getSelectedEdge = function() { | |||||
| for (var edgeId in this.selectionObj.edges) { | |||||
| if (this.selectionObj.edges.hasOwnProperty(edgeId)) { | |||||
| return this.selectionObj.edges[edgeId]; | |||||
| } | |||||
| } | |||||
| return null; | |||||
| }; | |||||
| /** | |||||
| * return the number of selected edges | |||||
| * | |||||
| * @returns {number} | |||||
| * @private | |||||
| */ | |||||
| exports._getSelectedEdgeCount = function() { | |||||
| var count = 0; | |||||
| for (var edgeId in this.selectionObj.edges) { | |||||
| if (this.selectionObj.edges.hasOwnProperty(edgeId)) { | |||||
| count += 1; | |||||
| } | |||||
| } | |||||
| return count; | |||||
| }; | |||||
| /** | |||||
| * return the number of selected objects. | |||||
| * | |||||
| * @returns {number} | |||||
| * @private | |||||
| */ | |||||
| exports._getSelectedObjectCount = function() { | |||||
| var count = 0; | |||||
| for(var nodeId in this.selectionObj.nodes) { | |||||
| if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { | |||||
| count += 1; | |||||
| } | |||||
| } | |||||
| for(var edgeId in this.selectionObj.edges) { | |||||
| if(this.selectionObj.edges.hasOwnProperty(edgeId)) { | |||||
| count += 1; | |||||
| } | |||||
| } | |||||
| return count; | |||||
| }; | |||||
| /** | |||||
| * Check if anything is selected | |||||
| * | |||||
| * @returns {boolean} | |||||
| * @private | |||||
| */ | |||||
| exports._selectionIsEmpty = function() { | |||||
| for(var nodeId in this.selectionObj.nodes) { | |||||
| if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { | |||||
| return false; | |||||
| } | |||||
| } | |||||
| for(var edgeId in this.selectionObj.edges) { | |||||
| if(this.selectionObj.edges.hasOwnProperty(edgeId)) { | |||||
| return false; | |||||
| } | |||||
| } | |||||
| return true; | |||||
| }; | |||||
| /** | |||||
| * check if one of the selected nodes is a cluster. | |||||
| * | |||||
| * @returns {boolean} | |||||
| * @private | |||||
| */ | |||||
| exports._clusterInSelection = function() { | |||||
| for(var nodeId in this.selectionObj.nodes) { | |||||
| if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { | |||||
| if (this.selectionObj.nodes[nodeId].clusterSize > 1) { | |||||
| return true; | |||||
| } | |||||
| } | |||||
| } | |||||
| return false; | |||||
| }; | |||||
| /** | |||||
| * select the edges connected to the node that is being selected | |||||
| * | |||||
| * @param {Node} node | |||||
| * @private | |||||
| */ | |||||
| exports._selectConnectedEdges = function(node) { | |||||
| for (var i = 0; i < node.dynamicEdges.length; i++) { | |||||
| var edge = node.dynamicEdges[i]; | |||||
| edge.select(); | |||||
| this._addToSelection(edge); | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * select the edges connected to the node that is being selected | |||||
| * | |||||
| * @param {Node} node | |||||
| * @private | |||||
| */ | |||||
| exports._hoverConnectedEdges = function(node) { | |||||
| for (var i = 0; i < node.dynamicEdges.length; i++) { | |||||
| var edge = node.dynamicEdges[i]; | |||||
| edge.hover = true; | |||||
| this._addToHover(edge); | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * unselect the edges connected to the node that is being selected | |||||
| * | |||||
| * @param {Node} node | |||||
| * @private | |||||
| */ | |||||
| exports._unselectConnectedEdges = function(node) { | |||||
| for (var i = 0; i < node.dynamicEdges.length; i++) { | |||||
| var edge = node.dynamicEdges[i]; | |||||
| edge.unselect(); | |||||
| this._removeFromSelection(edge); | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * This is called when someone clicks on a node. either select or deselect it. | |||||
| * If there is an existing selection and we don't want to append to it, clear the existing selection | |||||
| * | |||||
| * @param {Node || Edge} object | |||||
| * @param {Boolean} append | |||||
| * @param {Boolean} [doNotTrigger] | ignore trigger | |||||
| * @private | |||||
| */ | |||||
| exports._selectObject = function(object, append, doNotTrigger, highlightEdges) { | |||||
| if (doNotTrigger === undefined) { | |||||
| doNotTrigger = false; | |||||
| } | |||||
| if (highlightEdges === undefined) { | |||||
| highlightEdges = true; | |||||
| } | |||||
| if (this._selectionIsEmpty() == false && append == false && this.forceAppendSelection == false) { | |||||
| this._unselectAll(true); | |||||
| } | |||||
| if (object.selected == false) { | |||||
| object.select(); | |||||
| this._addToSelection(object); | |||||
| if (object instanceof Node && this.blockConnectingEdgeSelection == false && highlightEdges == true) { | |||||
| this._selectConnectedEdges(object); | |||||
| } | |||||
| } | |||||
| else { | |||||
| object.unselect(); | |||||
| this._removeFromSelection(object); | |||||
| } | |||||
| if (doNotTrigger == false) { | |||||
| this.emit('select', this.getSelection()); | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * This is called when someone clicks on a node. either select or deselect it. | |||||
| * If there is an existing selection and we don't want to append to it, clear the existing selection | |||||
| * | |||||
| * @param {Node || Edge} object | |||||
| * @private | |||||
| */ | |||||
| exports._blurObject = function(object) { | |||||
| if (object.hover == true) { | |||||
| object.hover = false; | |||||
| this.emit("blurNode",{node:object.id}); | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * This is called when someone clicks on a node. either select or deselect it. | |||||
| * If there is an existing selection and we don't want to append to it, clear the existing selection | |||||
| * | |||||
| * @param {Node || Edge} object | |||||
| * @private | |||||
| */ | |||||
| exports._hoverObject = function(object) { | |||||
| if (object.hover == false) { | |||||
| object.hover = true; | |||||
| this._addToHover(object); | |||||
| if (object instanceof Node) { | |||||
| this.emit("hoverNode",{node:object.id}); | |||||
| } | |||||
| } | |||||
| if (object instanceof Node) { | |||||
| this._hoverConnectedEdges(object); | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * handles the selection part of the touch, only for navigation controls elements; | |||||
| * Touch is triggered before tap, also before hold. Hold triggers after a while. | |||||
| * This is the most responsive solution | |||||
| * | |||||
| * @param {Object} pointer | |||||
| * @private | |||||
| */ | |||||
| exports._handleTouch = function(pointer) { | |||||
| }; | |||||
| /** | |||||
| * handles the selection part of the tap; | |||||
| * | |||||
| * @param {Object} pointer | |||||
| * @private | |||||
| */ | |||||
| exports._handleTap = function(pointer) { | |||||
| var node = this._getNodeAt(pointer); | |||||
| if (node != null) { | |||||
| this._selectObject(node,false); | |||||
| } | |||||
| else { | |||||
| var edge = this._getEdgeAt(pointer); | |||||
| if (edge != null) { | |||||
| this._selectObject(edge,false); | |||||
| } | |||||
| else { | |||||
| this._unselectAll(); | |||||
| } | |||||
| } | |||||
| this.emit("click", this.getSelection()); | |||||
| this._redraw(); | |||||
| }; | |||||
| /** | |||||
| * handles the selection part of the double tap and opens a cluster if needed | |||||
| * | |||||
| * @param {Object} pointer | |||||
| * @private | |||||
| */ | |||||
| exports._handleDoubleTap = function(pointer) { | |||||
| var node = this._getNodeAt(pointer); | |||||
| if (node != null && node !== undefined) { | |||||
| // we reset the areaCenter here so the opening of the node will occur | |||||
| this.areaCenter = {"x" : this._XconvertDOMtoCanvas(pointer.x), | |||||
| "y" : this._YconvertDOMtoCanvas(pointer.y)}; | |||||
| this.openCluster(node); | |||||
| } | |||||
| this.emit("doubleClick", this.getSelection()); | |||||
| }; | |||||
| /** | |||||
| * Handle the onHold selection part | |||||
| * | |||||
| * @param pointer | |||||
| * @private | |||||
| */ | |||||
| exports._handleOnHold = function(pointer) { | |||||
| var node = this._getNodeAt(pointer); | |||||
| if (node != null) { | |||||
| this._selectObject(node,true); | |||||
| } | |||||
| else { | |||||
| var edge = this._getEdgeAt(pointer); | |||||
| if (edge != null) { | |||||
| this._selectObject(edge,true); | |||||
| } | |||||
| } | |||||
| this._redraw(); | |||||
| }; | |||||
| /** | |||||
| * handle the onRelease event. These functions are here for the navigation controls module. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._handleOnRelease = function(pointer) { | |||||
| }; | |||||
| /** | |||||
| * | |||||
| * retrieve the currently selected objects | |||||
| * @return {{nodes: Array.<String>, edges: Array.<String>}} selection | |||||
| */ | |||||
| exports.getSelection = function() { | |||||
| var nodeIds = this.getSelectedNodes(); | |||||
| var edgeIds = this.getSelectedEdges(); | |||||
| return {nodes:nodeIds, edges:edgeIds}; | |||||
| }; | |||||
| /** | |||||
| * | |||||
| * retrieve the currently selected nodes | |||||
| * @return {String[]} selection An array with the ids of the | |||||
| * selected nodes. | |||||
| */ | |||||
| exports.getSelectedNodes = function() { | |||||
| var idArray = []; | |||||
| for(var nodeId in this.selectionObj.nodes) { | |||||
| if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { | |||||
| idArray.push(nodeId); | |||||
| } | |||||
| } | |||||
| return idArray | |||||
| }; | |||||
| /** | |||||
| * | |||||
| * retrieve the currently selected edges | |||||
| * @return {Array} selection An array with the ids of the | |||||
| * selected nodes. | |||||
| */ | |||||
| exports.getSelectedEdges = function() { | |||||
| var idArray = []; | |||||
| for(var edgeId in this.selectionObj.edges) { | |||||
| if(this.selectionObj.edges.hasOwnProperty(edgeId)) { | |||||
| idArray.push(edgeId); | |||||
| } | |||||
| } | |||||
| return idArray; | |||||
| }; | |||||
| /** | |||||
| * select zero or more nodes | |||||
| * @param {Number[] | String[]} selection An array with the ids of the | |||||
| * selected nodes. | |||||
| */ | |||||
| exports.setSelection = function(selection) { | |||||
| var i, iMax, id; | |||||
| if (!selection || (selection.length == undefined)) | |||||
| throw 'Selection must be an array with ids'; | |||||
| // first unselect any selected node | |||||
| this._unselectAll(true); | |||||
| for (i = 0, iMax = selection.length; i < iMax; i++) { | |||||
| id = selection[i]; | |||||
| var node = this.nodes[id]; | |||||
| if (!node) { | |||||
| throw new RangeError('Node with id "' + id + '" not found'); | |||||
| } | |||||
| this._selectObject(node,true,true); | |||||
| } | |||||
| console.log("setSelection is deprecated. Please use selectNodes instead.") | |||||
| this.redraw(); | |||||
| }; | |||||
| /** | |||||
| * select zero or more nodes with the option to highlight edges | |||||
| * @param {Number[] | String[]} selection An array with the ids of the | |||||
| * selected nodes. | |||||
| * @param {boolean} [highlightEdges] | |||||
| */ | |||||
| exports.selectNodes = function(selection, highlightEdges) { | |||||
| var i, iMax, id; | |||||
| if (!selection || (selection.length == undefined)) | |||||
| throw 'Selection must be an array with ids'; | |||||
| // first unselect any selected node | |||||
| this._unselectAll(true); | |||||
| for (i = 0, iMax = selection.length; i < iMax; i++) { | |||||
| id = selection[i]; | |||||
| var node = this.nodes[id]; | |||||
| if (!node) { | |||||
| throw new RangeError('Node with id "' + id + '" not found'); | |||||
| } | |||||
| this._selectObject(node,true,true,highlightEdges); | |||||
| } | |||||
| this.redraw(); | |||||
| }; | |||||
| /** | |||||
| * select zero or more edges | |||||
| * @param {Number[] | String[]} selection An array with the ids of the | |||||
| * selected nodes. | |||||
| */ | |||||
| exports.selectEdges = function(selection) { | |||||
| var i, iMax, id; | |||||
| if (!selection || (selection.length == undefined)) | |||||
| throw 'Selection must be an array with ids'; | |||||
| // first unselect any selected node | |||||
| this._unselectAll(true); | |||||
| for (i = 0, iMax = selection.length; i < iMax; i++) { | |||||
| id = selection[i]; | |||||
| var edge = this.edges[id]; | |||||
| if (!edge) { | |||||
| throw new RangeError('Edge with id "' + id + '" not found'); | |||||
| } | |||||
| this._selectObject(edge,true,true,highlightEdges); | |||||
| } | |||||
| this.redraw(); | |||||
| }; | |||||
| /** | |||||
| * Validate the selection: remove ids of nodes which no longer exist | |||||
| * @private | |||||
| */ | |||||
| exports._updateSelection = function () { | |||||
| for(var nodeId in this.selectionObj.nodes) { | |||||
| if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { | |||||
| if (!this.nodes.hasOwnProperty(nodeId)) { | |||||
| delete this.selectionObj.nodes[nodeId]; | |||||
| } | |||||
| } | |||||
| } | |||||
| for(var edgeId in this.selectionObj.edges) { | |||||
| if(this.selectionObj.edges.hasOwnProperty(edgeId)) { | |||||
| if (!this.edges.hasOwnProperty(edgeId)) { | |||||
| delete this.selectionObj.edges[edgeId]; | |||||
| } | |||||
| } | |||||
| } | |||||
| }; | |||||
| @ -0,0 +1,393 @@ | |||||
| /** | |||||
| * This function calculates the forces the nodes apply on eachother based on a gravitational model. | |||||
| * The Barnes Hut method is used to speed up this N-body simulation. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._calculateNodeForces = function() { | |||||
| if (this.constants.physics.barnesHut.gravitationalConstant != 0) { | |||||
| var node; | |||||
| var nodes = this.calculationNodes; | |||||
| var nodeIndices = this.calculationNodeIndices; | |||||
| var nodeCount = nodeIndices.length; | |||||
| this._formBarnesHutTree(nodes,nodeIndices); | |||||
| var barnesHutTree = this.barnesHutTree; | |||||
| // place the nodes one by one recursively | |||||
| for (var i = 0; i < nodeCount; i++) { | |||||
| node = nodes[nodeIndices[i]]; | |||||
| // starting with root is irrelevant, it never passes the BarnesHut condition | |||||
| this._getForceContribution(barnesHutTree.root.children.NW,node); | |||||
| this._getForceContribution(barnesHutTree.root.children.NE,node); | |||||
| this._getForceContribution(barnesHutTree.root.children.SW,node); | |||||
| this._getForceContribution(barnesHutTree.root.children.SE,node); | |||||
| } | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * This function traverses the barnesHutTree. It checks when it can approximate distant nodes with their center of mass. | |||||
| * If a region contains a single node, we check if it is not itself, then we apply the force. | |||||
| * | |||||
| * @param parentBranch | |||||
| * @param node | |||||
| * @private | |||||
| */ | |||||
| exports._getForceContribution = function(parentBranch,node) { | |||||
| // we get no force contribution from an empty region | |||||
| if (parentBranch.childrenCount > 0) { | |||||
| var dx,dy,distance; | |||||
| // get the distance from the center of mass to the node. | |||||
| dx = parentBranch.centerOfMass.x - node.x; | |||||
| dy = parentBranch.centerOfMass.y - node.y; | |||||
| distance = Math.sqrt(dx * dx + dy * dy); | |||||
| // BarnesHut condition | |||||
| // original condition : s/d < theta = passed === d/s > 1/theta = passed | |||||
| // calcSize = 1/s --> d * 1/s > 1/theta = passed | |||||
| if (distance * parentBranch.calcSize > this.constants.physics.barnesHut.theta) { | |||||
| // duplicate code to reduce function calls to speed up program | |||||
| if (distance == 0) { | |||||
| distance = 0.1*Math.random(); | |||||
| dx = distance; | |||||
| } | |||||
| var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.mass / (distance * distance * distance); | |||||
| var fx = dx * gravityForce; | |||||
| var fy = dy * gravityForce; | |||||
| node.fx += fx; | |||||
| node.fy += fy; | |||||
| } | |||||
| else { | |||||
| // Did not pass the condition, go into children if available | |||||
| if (parentBranch.childrenCount == 4) { | |||||
| this._getForceContribution(parentBranch.children.NW,node); | |||||
| this._getForceContribution(parentBranch.children.NE,node); | |||||
| this._getForceContribution(parentBranch.children.SW,node); | |||||
| this._getForceContribution(parentBranch.children.SE,node); | |||||
| } | |||||
| else { // parentBranch must have only one node, if it was empty we wouldnt be here | |||||
| if (parentBranch.children.data.id != node.id) { // if it is not self | |||||
| // duplicate code to reduce function calls to speed up program | |||||
| if (distance == 0) { | |||||
| distance = 0.5*Math.random(); | |||||
| dx = distance; | |||||
| } | |||||
| var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.mass / (distance * distance * distance); | |||||
| var fx = dx * gravityForce; | |||||
| var fy = dy * gravityForce; | |||||
| node.fx += fx; | |||||
| node.fy += fy; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes. | |||||
| * | |||||
| * @param nodes | |||||
| * @param nodeIndices | |||||
| * @private | |||||
| */ | |||||
| exports._formBarnesHutTree = function(nodes,nodeIndices) { | |||||
| var node; | |||||
| var nodeCount = nodeIndices.length; | |||||
| var minX = Number.MAX_VALUE, | |||||
| minY = Number.MAX_VALUE, | |||||
| maxX =-Number.MAX_VALUE, | |||||
| maxY =-Number.MAX_VALUE; | |||||
| // get the range of the nodes | |||||
| for (var i = 0; i < nodeCount; i++) { | |||||
| var x = nodes[nodeIndices[i]].x; | |||||
| var y = nodes[nodeIndices[i]].y; | |||||
| if (x < minX) { minX = x; } | |||||
| if (x > maxX) { maxX = x; } | |||||
| if (y < minY) { minY = y; } | |||||
| if (y > maxY) { maxY = y; } | |||||
| } | |||||
| // make the range a square | |||||
| var sizeDiff = Math.abs(maxX - minX) - Math.abs(maxY - minY); // difference between X and Y | |||||
| if (sizeDiff > 0) {minY -= 0.5 * sizeDiff; maxY += 0.5 * sizeDiff;} // xSize > ySize | |||||
| else {minX += 0.5 * sizeDiff; maxX -= 0.5 * sizeDiff;} // xSize < ySize | |||||
| var minimumTreeSize = 1e-5; | |||||
| var rootSize = Math.max(minimumTreeSize,Math.abs(maxX - minX)); | |||||
| var halfRootSize = 0.5 * rootSize; | |||||
| var centerX = 0.5 * (minX + maxX), centerY = 0.5 * (minY + maxY); | |||||
| // construct the barnesHutTree | |||||
| var barnesHutTree = { | |||||
| root:{ | |||||
| centerOfMass: {x:0, y:0}, | |||||
| mass:0, | |||||
| range: { | |||||
| minX: centerX-halfRootSize,maxX:centerX+halfRootSize, | |||||
| minY: centerY-halfRootSize,maxY:centerY+halfRootSize | |||||
| }, | |||||
| size: rootSize, | |||||
| calcSize: 1 / rootSize, | |||||
| children: { data:null}, | |||||
| maxWidth: 0, | |||||
| level: 0, | |||||
| childrenCount: 4 | |||||
| } | |||||
| }; | |||||
| this._splitBranch(barnesHutTree.root); | |||||
| // place the nodes one by one recursively | |||||
| for (i = 0; i < nodeCount; i++) { | |||||
| node = nodes[nodeIndices[i]]; | |||||
| this._placeInTree(barnesHutTree.root,node); | |||||
| } | |||||
| // make global | |||||
| this.barnesHutTree = barnesHutTree | |||||
| }; | |||||
| /** | |||||
| * this updates the mass of a branch. this is increased by adding a node. | |||||
| * | |||||
| * @param parentBranch | |||||
| * @param node | |||||
| * @private | |||||
| */ | |||||
| exports._updateBranchMass = function(parentBranch, node) { | |||||
| var totalMass = parentBranch.mass + node.mass; | |||||
| var totalMassInv = 1/totalMass; | |||||
| parentBranch.centerOfMass.x = parentBranch.centerOfMass.x * parentBranch.mass + node.x * node.mass; | |||||
| parentBranch.centerOfMass.x *= totalMassInv; | |||||
| parentBranch.centerOfMass.y = parentBranch.centerOfMass.y * parentBranch.mass + node.y * node.mass; | |||||
| parentBranch.centerOfMass.y *= totalMassInv; | |||||
| parentBranch.mass = totalMass; | |||||
| var biggestSize = Math.max(Math.max(node.height,node.radius),node.width); | |||||
| parentBranch.maxWidth = (parentBranch.maxWidth < biggestSize) ? biggestSize : parentBranch.maxWidth; | |||||
| }; | |||||
| /** | |||||
| * determine in which branch the node will be placed. | |||||
| * | |||||
| * @param parentBranch | |||||
| * @param node | |||||
| * @param skipMassUpdate | |||||
| * @private | |||||
| */ | |||||
| exports._placeInTree = function(parentBranch,node,skipMassUpdate) { | |||||
| if (skipMassUpdate != true || skipMassUpdate === undefined) { | |||||
| // update the mass of the branch. | |||||
| this._updateBranchMass(parentBranch,node); | |||||
| } | |||||
| if (parentBranch.children.NW.range.maxX > node.x) { // in NW or SW | |||||
| if (parentBranch.children.NW.range.maxY > node.y) { // in NW | |||||
| this._placeInRegion(parentBranch,node,"NW"); | |||||
| } | |||||
| else { // in SW | |||||
| this._placeInRegion(parentBranch,node,"SW"); | |||||
| } | |||||
| } | |||||
| else { // in NE or SE | |||||
| if (parentBranch.children.NW.range.maxY > node.y) { // in NE | |||||
| this._placeInRegion(parentBranch,node,"NE"); | |||||
| } | |||||
| else { // in SE | |||||
| this._placeInRegion(parentBranch,node,"SE"); | |||||
| } | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * actually place the node in a region (or branch) | |||||
| * | |||||
| * @param parentBranch | |||||
| * @param node | |||||
| * @param region | |||||
| * @private | |||||
| */ | |||||
| exports._placeInRegion = function(parentBranch,node,region) { | |||||
| switch (parentBranch.children[region].childrenCount) { | |||||
| case 0: // place node here | |||||
| parentBranch.children[region].children.data = node; | |||||
| parentBranch.children[region].childrenCount = 1; | |||||
| this._updateBranchMass(parentBranch.children[region],node); | |||||
| break; | |||||
| case 1: // convert into children | |||||
| // if there are two nodes exactly overlapping (on init, on opening of cluster etc.) | |||||
| // we move one node a pixel and we do not put it in the tree. | |||||
| if (parentBranch.children[region].children.data.x == node.x && | |||||
| parentBranch.children[region].children.data.y == node.y) { | |||||
| node.x += Math.random(); | |||||
| node.y += Math.random(); | |||||
| } | |||||
| else { | |||||
| this._splitBranch(parentBranch.children[region]); | |||||
| this._placeInTree(parentBranch.children[region],node); | |||||
| } | |||||
| break; | |||||
| case 4: // place in branch | |||||
| this._placeInTree(parentBranch.children[region],node); | |||||
| break; | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * this function splits a branch into 4 sub branches. If the branch contained a node, we place it in the subbranch | |||||
| * after the split is complete. | |||||
| * | |||||
| * @param parentBranch | |||||
| * @private | |||||
| */ | |||||
| exports._splitBranch = function(parentBranch) { | |||||
| // if the branch is shaded with a node, replace the node in the new subset. | |||||
| var containedNode = null; | |||||
| if (parentBranch.childrenCount == 1) { | |||||
| containedNode = parentBranch.children.data; | |||||
| parentBranch.mass = 0; parentBranch.centerOfMass.x = 0; parentBranch.centerOfMass.y = 0; | |||||
| } | |||||
| parentBranch.childrenCount = 4; | |||||
| parentBranch.children.data = null; | |||||
| this._insertRegion(parentBranch,"NW"); | |||||
| this._insertRegion(parentBranch,"NE"); | |||||
| this._insertRegion(parentBranch,"SW"); | |||||
| this._insertRegion(parentBranch,"SE"); | |||||
| if (containedNode != null) { | |||||
| this._placeInTree(parentBranch,containedNode); | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * This function subdivides the region into four new segments. | |||||
| * Specifically, this inserts a single new segment. | |||||
| * It fills the children section of the parentBranch | |||||
| * | |||||
| * @param parentBranch | |||||
| * @param region | |||||
| * @param parentRange | |||||
| * @private | |||||
| */ | |||||
| exports._insertRegion = function(parentBranch, region) { | |||||
| var minX,maxX,minY,maxY; | |||||
| var childSize = 0.5 * parentBranch.size; | |||||
| switch (region) { | |||||
| case "NW": | |||||
| minX = parentBranch.range.minX; | |||||
| maxX = parentBranch.range.minX + childSize; | |||||
| minY = parentBranch.range.minY; | |||||
| maxY = parentBranch.range.minY + childSize; | |||||
| break; | |||||
| case "NE": | |||||
| minX = parentBranch.range.minX + childSize; | |||||
| maxX = parentBranch.range.maxX; | |||||
| minY = parentBranch.range.minY; | |||||
| maxY = parentBranch.range.minY + childSize; | |||||
| break; | |||||
| case "SW": | |||||
| minX = parentBranch.range.minX; | |||||
| maxX = parentBranch.range.minX + childSize; | |||||
| minY = parentBranch.range.minY + childSize; | |||||
| maxY = parentBranch.range.maxY; | |||||
| break; | |||||
| case "SE": | |||||
| minX = parentBranch.range.minX + childSize; | |||||
| maxX = parentBranch.range.maxX; | |||||
| minY = parentBranch.range.minY + childSize; | |||||
| maxY = parentBranch.range.maxY; | |||||
| break; | |||||
| } | |||||
| parentBranch.children[region] = { | |||||
| centerOfMass:{x:0,y:0}, | |||||
| mass:0, | |||||
| range:{minX:minX,maxX:maxX,minY:minY,maxY:maxY}, | |||||
| size: 0.5 * parentBranch.size, | |||||
| calcSize: 2 * parentBranch.calcSize, | |||||
| children: {data:null}, | |||||
| maxWidth: 0, | |||||
| level: parentBranch.level+1, | |||||
| childrenCount: 0 | |||||
| }; | |||||
| }; | |||||
| /** | |||||
| * This function is for debugging purposed, it draws the tree. | |||||
| * | |||||
| * @param ctx | |||||
| * @param color | |||||
| * @private | |||||
| */ | |||||
| exports._drawTree = function(ctx,color) { | |||||
| if (this.barnesHutTree !== undefined) { | |||||
| ctx.lineWidth = 1; | |||||
| this._drawBranch(this.barnesHutTree.root,ctx,color); | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * This function is for debugging purposes. It draws the branches recursively. | |||||
| * | |||||
| * @param branch | |||||
| * @param ctx | |||||
| * @param color | |||||
| * @private | |||||
| */ | |||||
| exports._drawBranch = function(branch,ctx,color) { | |||||
| if (color === undefined) { | |||||
| color = "#FF0000"; | |||||
| } | |||||
| if (branch.childrenCount == 4) { | |||||
| this._drawBranch(branch.children.NW,ctx); | |||||
| this._drawBranch(branch.children.NE,ctx); | |||||
| this._drawBranch(branch.children.SE,ctx); | |||||
| this._drawBranch(branch.children.SW,ctx); | |||||
| } | |||||
| ctx.strokeStyle = color; | |||||
| ctx.beginPath(); | |||||
| ctx.moveTo(branch.range.minX,branch.range.minY); | |||||
| ctx.lineTo(branch.range.maxX,branch.range.minY); | |||||
| ctx.stroke(); | |||||
| ctx.beginPath(); | |||||
| ctx.moveTo(branch.range.maxX,branch.range.minY); | |||||
| ctx.lineTo(branch.range.maxX,branch.range.maxY); | |||||
| ctx.stroke(); | |||||
| ctx.beginPath(); | |||||
| ctx.moveTo(branch.range.maxX,branch.range.maxY); | |||||
| ctx.lineTo(branch.range.minX,branch.range.maxY); | |||||
| ctx.stroke(); | |||||
| ctx.beginPath(); | |||||
| ctx.moveTo(branch.range.minX,branch.range.maxY); | |||||
| ctx.lineTo(branch.range.minX,branch.range.minY); | |||||
| ctx.stroke(); | |||||
| /* | |||||
| if (branch.mass > 0) { | |||||
| ctx.circle(branch.centerOfMass.x, branch.centerOfMass.y, 3*branch.mass); | |||||
| ctx.stroke(); | |||||
| } | |||||
| */ | |||||
| }; | |||||
| @ -0,0 +1,125 @@ | |||||
| /** | |||||
| * Calculate the forces the nodes apply on eachother based on a repulsion field. | |||||
| * This field is linearly approximated. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._calculateNodeForces = function () { | |||||
| var dx, dy, distance, fx, fy, combinedClusterSize, | |||||
| repulsingForce, node1, node2, i, j; | |||||
| var nodes = this.calculationNodes; | |||||
| var nodeIndices = this.calculationNodeIndices; | |||||
| // approximation constants | |||||
| var b = 5; | |||||
| var a_base = 0.5 * -b; | |||||
| // repulsing forces between nodes | |||||
| var nodeDistance = this.constants.physics.hierarchicalRepulsion.nodeDistance; | |||||
| var minimumDistance = nodeDistance; | |||||
| var a = a_base / minimumDistance; | |||||
| // we loop from i over all but the last entree in the array | |||||
| // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j | |||||
| for (i = 0; i < nodeIndices.length - 1; i++) { | |||||
| node1 = nodes[nodeIndices[i]]; | |||||
| for (j = i + 1; j < nodeIndices.length; j++) { | |||||
| node2 = nodes[nodeIndices[j]]; | |||||
| if (node1.level == node2.level) { | |||||
| dx = node2.x - node1.x; | |||||
| dy = node2.y - node1.y; | |||||
| distance = Math.sqrt(dx * dx + dy * dy); | |||||
| if (distance < 2 * minimumDistance) { | |||||
| repulsingForce = a * distance + b; | |||||
| var c = 0.05; | |||||
| var d = 2 * minimumDistance * 2 * c; | |||||
| repulsingForce = c * Math.pow(distance,2) - d * distance + d*d/(4*c); | |||||
| // normalize force with | |||||
| if (distance == 0) { | |||||
| distance = 0.01; | |||||
| } | |||||
| else { | |||||
| repulsingForce = repulsingForce / distance; | |||||
| } | |||||
| fx = dx * repulsingForce; | |||||
| fy = dy * repulsingForce; | |||||
| node1.fx -= fx; | |||||
| node1.fy -= fy; | |||||
| node2.fx += fx; | |||||
| node2.fy += fy; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * this function calculates the effects of the springs in the case of unsmooth curves. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._calculateHierarchicalSpringForces = function () { | |||||
| var edgeLength, edge, edgeId; | |||||
| var dx, dy, fx, fy, springForce, distance; | |||||
| var edges = this.edges; | |||||
| // forces caused by the edges, modelled as springs | |||||
| for (edgeId in edges) { | |||||
| if (edges.hasOwnProperty(edgeId)) { | |||||
| edge = edges[edgeId]; | |||||
| if (edge.connected) { | |||||
| // only calculate forces if nodes are in the same sector | |||||
| if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) { | |||||
| edgeLength = edge.customLength ? edge.length : this.constants.physics.springLength; | |||||
| // this implies that the edges between big clusters are longer | |||||
| edgeLength += (edge.to.clusterSize + edge.from.clusterSize - 2) * this.constants.clustering.edgeGrowth; | |||||
| dx = (edge.from.x - edge.to.x); | |||||
| dy = (edge.from.y - edge.to.y); | |||||
| distance = Math.sqrt(dx * dx + dy * dy); | |||||
| if (distance == 0) { | |||||
| distance = 0.01; | |||||
| } | |||||
| distance = Math.max(0.8*edgeLength,Math.min(5*edgeLength, distance)); | |||||
| // the 1/distance is so the fx and fy can be calculated without sine or cosine. | |||||
| springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance; | |||||
| fx = dx * springForce; | |||||
| fy = dy * springForce; | |||||
| edge.to.fx -= fx; | |||||
| edge.to.fy -= fy; | |||||
| edge.from.fx += fx; | |||||
| edge.from.fy += fy; | |||||
| var factor = 5; | |||||
| if (distance > edgeLength) { | |||||
| factor = 25; | |||||
| } | |||||
| if (edge.from.level > edge.to.level) { | |||||
| edge.to.fx -= factor*fx; | |||||
| edge.to.fy -= factor*fy; | |||||
| } | |||||
| else if (edge.from.level < edge.to.level) { | |||||
| edge.from.fx += factor*fx; | |||||
| edge.from.fy += factor*fy; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| }; | |||||
| @ -0,0 +1,700 @@ | |||||
| var util = require('../../../util'); | |||||
| var RepulsionMixin = require('./RepulsionMixin'); | |||||
| var HierarchialRepulsionMixin = require('./HierarchialRepulsionMixin'); | |||||
| var BarnesHutMixin = require('./BarnesHutMixin'); | |||||
| /** | |||||
| * Toggling barnes Hut calculation on and off. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._toggleBarnesHut = function () { | |||||
| this.constants.physics.barnesHut.enabled = !this.constants.physics.barnesHut.enabled; | |||||
| this._loadSelectedForceSolver(); | |||||
| this.moving = true; | |||||
| this.start(); | |||||
| }; | |||||
| /** | |||||
| * This loads the node force solver based on the barnes hut or repulsion algorithm | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._loadSelectedForceSolver = function () { | |||||
| // this overloads the this._calculateNodeForces | |||||
| if (this.constants.physics.barnesHut.enabled == true) { | |||||
| this._clearMixin(RepulsionMixin); | |||||
| this._clearMixin(HierarchialRepulsionMixin); | |||||
| this.constants.physics.centralGravity = this.constants.physics.barnesHut.centralGravity; | |||||
| this.constants.physics.springLength = this.constants.physics.barnesHut.springLength; | |||||
| this.constants.physics.springConstant = this.constants.physics.barnesHut.springConstant; | |||||
| this.constants.physics.damping = this.constants.physics.barnesHut.damping; | |||||
| this._loadMixin(BarnesHutMixin); | |||||
| } | |||||
| else if (this.constants.physics.hierarchicalRepulsion.enabled == true) { | |||||
| this._clearMixin(BarnesHutMixin); | |||||
| this._clearMixin(RepulsionMixin); | |||||
| this.constants.physics.centralGravity = this.constants.physics.hierarchicalRepulsion.centralGravity; | |||||
| this.constants.physics.springLength = this.constants.physics.hierarchicalRepulsion.springLength; | |||||
| this.constants.physics.springConstant = this.constants.physics.hierarchicalRepulsion.springConstant; | |||||
| this.constants.physics.damping = this.constants.physics.hierarchicalRepulsion.damping; | |||||
| this._loadMixin(HierarchialRepulsionMixin); | |||||
| } | |||||
| else { | |||||
| this._clearMixin(BarnesHutMixin); | |||||
| this._clearMixin(HierarchialRepulsionMixin); | |||||
| this.barnesHutTree = undefined; | |||||
| this.constants.physics.centralGravity = this.constants.physics.repulsion.centralGravity; | |||||
| this.constants.physics.springLength = this.constants.physics.repulsion.springLength; | |||||
| this.constants.physics.springConstant = this.constants.physics.repulsion.springConstant; | |||||
| this.constants.physics.damping = this.constants.physics.repulsion.damping; | |||||
| this._loadMixin(RepulsionMixin); | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Before calculating the forces, we check if we need to cluster to keep up performance and we check | |||||
| * if there is more than one node. If it is just one node, we dont calculate anything. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._initializeForceCalculation = function () { | |||||
| // stop calculation if there is only one node | |||||
| if (this.nodeIndices.length == 1) { | |||||
| this.nodes[this.nodeIndices[0]]._setForce(0, 0); | |||||
| } | |||||
| else { | |||||
| // if there are too many nodes on screen, we cluster without repositioning | |||||
| if (this.nodeIndices.length > this.constants.clustering.clusterThreshold && this.constants.clustering.enabled == true) { | |||||
| this.clusterToFit(this.constants.clustering.reduceToNodes, false); | |||||
| } | |||||
| // we now start the force calculation | |||||
| this._calculateForces(); | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Calculate the external forces acting on the nodes | |||||
| * Forces are caused by: edges, repulsing forces between nodes, gravity | |||||
| * @private | |||||
| */ | |||||
| exports._calculateForces = function () { | |||||
| // Gravity is required to keep separated groups from floating off | |||||
| // the forces are reset to zero in this loop by using _setForce instead | |||||
| // of _addForce | |||||
| this._calculateGravitationalForces(); | |||||
| this._calculateNodeForces(); | |||||
| if (this.constants.smoothCurves == true) { | |||||
| this._calculateSpringForcesWithSupport(); | |||||
| } | |||||
| else { | |||||
| if (this.constants.physics.hierarchicalRepulsion.enabled == true) { | |||||
| this._calculateHierarchicalSpringForces(); | |||||
| } | |||||
| else { | |||||
| this._calculateSpringForces(); | |||||
| } | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Smooth curves are created by adding invisible nodes in the center of the edges. These nodes are also | |||||
| * handled in the calculateForces function. We then use a quadratic curve with the center node as control. | |||||
| * This function joins the datanodes and invisible (called support) nodes into one object. | |||||
| * We do this so we do not contaminate this.nodes with the support nodes. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._updateCalculationNodes = function () { | |||||
| if (this.constants.smoothCurves == true) { | |||||
| this.calculationNodes = {}; | |||||
| this.calculationNodeIndices = []; | |||||
| for (var nodeId in this.nodes) { | |||||
| if (this.nodes.hasOwnProperty(nodeId)) { | |||||
| this.calculationNodes[nodeId] = this.nodes[nodeId]; | |||||
| } | |||||
| } | |||||
| var supportNodes = this.sectors['support']['nodes']; | |||||
| for (var supportNodeId in supportNodes) { | |||||
| if (supportNodes.hasOwnProperty(supportNodeId)) { | |||||
| if (this.edges.hasOwnProperty(supportNodes[supportNodeId].parentEdgeId)) { | |||||
| this.calculationNodes[supportNodeId] = supportNodes[supportNodeId]; | |||||
| } | |||||
| else { | |||||
| supportNodes[supportNodeId]._setForce(0, 0); | |||||
| } | |||||
| } | |||||
| } | |||||
| for (var idx in this.calculationNodes) { | |||||
| if (this.calculationNodes.hasOwnProperty(idx)) { | |||||
| this.calculationNodeIndices.push(idx); | |||||
| } | |||||
| } | |||||
| } | |||||
| else { | |||||
| this.calculationNodes = this.nodes; | |||||
| this.calculationNodeIndices = this.nodeIndices; | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * this function applies the central gravity effect to keep groups from floating off | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._calculateGravitationalForces = function () { | |||||
| var dx, dy, distance, node, i; | |||||
| var nodes = this.calculationNodes; | |||||
| var gravity = this.constants.physics.centralGravity; | |||||
| var gravityForce = 0; | |||||
| for (i = 0; i < this.calculationNodeIndices.length; i++) { | |||||
| node = nodes[this.calculationNodeIndices[i]]; | |||||
| node.damping = this.constants.physics.damping; // possibly add function to alter damping properties of clusters. | |||||
| // gravity does not apply when we are in a pocket sector | |||||
| if (this._sector() == "default" && gravity != 0) { | |||||
| dx = -node.x; | |||||
| dy = -node.y; | |||||
| distance = Math.sqrt(dx * dx + dy * dy); | |||||
| gravityForce = (distance == 0) ? 0 : (gravity / distance); | |||||
| node.fx = dx * gravityForce; | |||||
| node.fy = dy * gravityForce; | |||||
| } | |||||
| else { | |||||
| node.fx = 0; | |||||
| node.fy = 0; | |||||
| } | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * this function calculates the effects of the springs in the case of unsmooth curves. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._calculateSpringForces = function () { | |||||
| var edgeLength, edge, edgeId; | |||||
| var dx, dy, fx, fy, springForce, distance; | |||||
| var edges = this.edges; | |||||
| // forces caused by the edges, modelled as springs | |||||
| for (edgeId in edges) { | |||||
| if (edges.hasOwnProperty(edgeId)) { | |||||
| edge = edges[edgeId]; | |||||
| if (edge.connected) { | |||||
| // only calculate forces if nodes are in the same sector | |||||
| if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) { | |||||
| edgeLength = edge.customLength ? edge.length : this.constants.physics.springLength; | |||||
| // this implies that the edges between big clusters are longer | |||||
| edgeLength += (edge.to.clusterSize + edge.from.clusterSize - 2) * this.constants.clustering.edgeGrowth; | |||||
| dx = (edge.from.x - edge.to.x); | |||||
| dy = (edge.from.y - edge.to.y); | |||||
| distance = Math.sqrt(dx * dx + dy * dy); | |||||
| if (distance == 0) { | |||||
| distance = 0.01; | |||||
| } | |||||
| // the 1/distance is so the fx and fy can be calculated without sine or cosine. | |||||
| springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance; | |||||
| fx = dx * springForce; | |||||
| fy = dy * springForce; | |||||
| edge.from.fx += fx; | |||||
| edge.from.fy += fy; | |||||
| edge.to.fx -= fx; | |||||
| edge.to.fy -= fy; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * This function calculates the springforces on the nodes, accounting for the support nodes. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._calculateSpringForcesWithSupport = function () { | |||||
| var edgeLength, edge, edgeId, combinedClusterSize; | |||||
| var edges = this.edges; | |||||
| // forces caused by the edges, modelled as springs | |||||
| for (edgeId in edges) { | |||||
| if (edges.hasOwnProperty(edgeId)) { | |||||
| edge = edges[edgeId]; | |||||
| if (edge.connected) { | |||||
| // only calculate forces if nodes are in the same sector | |||||
| if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) { | |||||
| if (edge.via != null) { | |||||
| var node1 = edge.to; | |||||
| var node2 = edge.via; | |||||
| var node3 = edge.from; | |||||
| edgeLength = edge.customLength ? edge.length : this.constants.physics.springLength; | |||||
| combinedClusterSize = node1.clusterSize + node3.clusterSize - 2; | |||||
| // this implies that the edges between big clusters are longer | |||||
| edgeLength += combinedClusterSize * this.constants.clustering.edgeGrowth; | |||||
| this._calculateSpringForce(node1, node2, 0.5 * edgeLength); | |||||
| this._calculateSpringForce(node2, node3, 0.5 * edgeLength); | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * This is the code actually performing the calculation for the function above. It is split out to avoid repetition. | |||||
| * | |||||
| * @param node1 | |||||
| * @param node2 | |||||
| * @param edgeLength | |||||
| * @private | |||||
| */ | |||||
| exports._calculateSpringForce = function (node1, node2, edgeLength) { | |||||
| var dx, dy, fx, fy, springForce, distance; | |||||
| dx = (node1.x - node2.x); | |||||
| dy = (node1.y - node2.y); | |||||
| distance = Math.sqrt(dx * dx + dy * dy); | |||||
| if (distance == 0) { | |||||
| distance = 0.01; | |||||
| } | |||||
| // the 1/distance is so the fx and fy can be calculated without sine or cosine. | |||||
| springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance; | |||||
| fx = dx * springForce; | |||||
| fy = dy * springForce; | |||||
| node1.fx += fx; | |||||
| node1.fy += fy; | |||||
| node2.fx -= fx; | |||||
| node2.fy -= fy; | |||||
| }; | |||||
| /** | |||||
| * Load the HTML for the physics config and bind it | |||||
| * @private | |||||
| */ | |||||
| exports._loadPhysicsConfiguration = function () { | |||||
| if (this.physicsConfiguration === undefined) { | |||||
| this.backupConstants = {}; | |||||
| util.deepExtend(this.backupConstants,this.constants); | |||||
| var hierarchicalLayoutDirections = ["LR", "RL", "UD", "DU"]; | |||||
| this.physicsConfiguration = document.createElement('div'); | |||||
| this.physicsConfiguration.className = "PhysicsConfiguration"; | |||||
| this.physicsConfiguration.innerHTML = '' + | |||||
| '<table><tr><td><b>Simulation Mode:</b></td></tr>' + | |||||
| '<tr>' + | |||||
| '<td width="120px"><input type="radio" name="graph_physicsMethod" id="graph_physicsMethod1" value="BH" checked="checked">Barnes Hut</td>' + | |||||
| '<td width="120px"><input type="radio" name="graph_physicsMethod" id="graph_physicsMethod2" value="R">Repulsion</td>' + | |||||
| '<td width="120px"><input type="radio" name="graph_physicsMethod" id="graph_physicsMethod3" value="H">Hierarchical</td>' + | |||||
| '</tr>' + | |||||
| '</table>' + | |||||
| '<table id="graph_BH_table" style="display:none">' + | |||||
| '<tr><td><b>Barnes Hut</b></td></tr>' + | |||||
| '<tr>' + | |||||
| '<td width="150px">gravitationalConstant</td><td>0</td><td><input type="range" min="0" max="20000" value="' + (-1 * this.constants.physics.barnesHut.gravitationalConstant) + '" step="25" style="width:300px" id="graph_BH_gc"></td><td width="50px">-20000</td><td><input value="' + (-1 * this.constants.physics.barnesHut.gravitationalConstant) + '" id="graph_BH_gc_value" style="width:60px"></td>' + | |||||
| '</tr>' + | |||||
| '<tr>' + | |||||
| '<td width="150px">centralGravity</td><td>0</td><td><input type="range" min="0" max="3" value="' + this.constants.physics.barnesHut.centralGravity + '" step="0.05" style="width:300px" id="graph_BH_cg"></td><td>3</td><td><input value="' + this.constants.physics.barnesHut.centralGravity + '" id="graph_BH_cg_value" style="width:60px"></td>' + | |||||
| '</tr>' + | |||||
| '<tr>' + | |||||
| '<td width="150px">springLength</td><td>0</td><td><input type="range" min="0" max="500" value="' + this.constants.physics.barnesHut.springLength + '" step="1" style="width:300px" id="graph_BH_sl"></td><td>500</td><td><input value="' + this.constants.physics.barnesHut.springLength + '" id="graph_BH_sl_value" style="width:60px"></td>' + | |||||
| '</tr>' + | |||||
| '<tr>' + | |||||
| '<td width="150px">springConstant</td><td>0</td><td><input type="range" min="0" max="0.5" value="' + this.constants.physics.barnesHut.springConstant + '" step="0.001" style="width:300px" id="graph_BH_sc"></td><td>0.5</td><td><input value="' + this.constants.physics.barnesHut.springConstant + '" id="graph_BH_sc_value" style="width:60px"></td>' + | |||||
| '</tr>' + | |||||
| '<tr>' + | |||||
| '<td width="150px">damping</td><td>0</td><td><input type="range" min="0" max="0.3" value="' + this.constants.physics.barnesHut.damping + '" step="0.005" style="width:300px" id="graph_BH_damp"></td><td>0.3</td><td><input value="' + this.constants.physics.barnesHut.damping + '" id="graph_BH_damp_value" style="width:60px"></td>' + | |||||
| '</tr>' + | |||||
| '</table>' + | |||||
| '<table id="graph_R_table" style="display:none">' + | |||||
| '<tr><td><b>Repulsion</b></td></tr>' + | |||||
| '<tr>' + | |||||
| '<td width="150px">nodeDistance</td><td>0</td><td><input type="range" min="0" max="300" value="' + this.constants.physics.repulsion.nodeDistance + '" step="1" style="width:300px" id="graph_R_nd"></td><td width="50px">300</td><td><input value="' + this.constants.physics.repulsion.nodeDistance + '" id="graph_R_nd_value" style="width:60px"></td>' + | |||||
| '</tr>' + | |||||
| '<tr>' + | |||||
| '<td width="150px">centralGravity</td><td>0</td><td><input type="range" min="0" max="3" value="' + this.constants.physics.repulsion.centralGravity + '" step="0.05" style="width:300px" id="graph_R_cg"></td><td>3</td><td><input value="' + this.constants.physics.repulsion.centralGravity + '" id="graph_R_cg_value" style="width:60px"></td>' + | |||||
| '</tr>' + | |||||
| '<tr>' + | |||||
| '<td width="150px">springLength</td><td>0</td><td><input type="range" min="0" max="500" value="' + this.constants.physics.repulsion.springLength + '" step="1" style="width:300px" id="graph_R_sl"></td><td>500</td><td><input value="' + this.constants.physics.repulsion.springLength + '" id="graph_R_sl_value" style="width:60px"></td>' + | |||||
| '</tr>' + | |||||
| '<tr>' + | |||||
| '<td width="150px">springConstant</td><td>0</td><td><input type="range" min="0" max="0.5" value="' + this.constants.physics.repulsion.springConstant + '" step="0.001" style="width:300px" id="graph_R_sc"></td><td>0.5</td><td><input value="' + this.constants.physics.repulsion.springConstant + '" id="graph_R_sc_value" style="width:60px"></td>' + | |||||
| '</tr>' + | |||||
| '<tr>' + | |||||
| '<td width="150px">damping</td><td>0</td><td><input type="range" min="0" max="0.3" value="' + this.constants.physics.repulsion.damping + '" step="0.005" style="width:300px" id="graph_R_damp"></td><td>0.3</td><td><input value="' + this.constants.physics.repulsion.damping + '" id="graph_R_damp_value" style="width:60px"></td>' + | |||||
| '</tr>' + | |||||
| '</table>' + | |||||
| '<table id="graph_H_table" style="display:none">' + | |||||
| '<tr><td width="150"><b>Hierarchical</b></td></tr>' + | |||||
| '<tr>' + | |||||
| '<td width="150px">nodeDistance</td><td>0</td><td><input type="range" min="0" max="300" value="' + this.constants.physics.hierarchicalRepulsion.nodeDistance + '" step="1" style="width:300px" id="graph_H_nd"></td><td width="50px">300</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.nodeDistance + '" id="graph_H_nd_value" style="width:60px"></td>' + | |||||
| '</tr>' + | |||||
| '<tr>' + | |||||
| '<td width="150px">centralGravity</td><td>0</td><td><input type="range" min="0" max="3" value="' + this.constants.physics.hierarchicalRepulsion.centralGravity + '" step="0.05" style="width:300px" id="graph_H_cg"></td><td>3</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.centralGravity + '" id="graph_H_cg_value" style="width:60px"></td>' + | |||||
| '</tr>' + | |||||
| '<tr>' + | |||||
| '<td width="150px">springLength</td><td>0</td><td><input type="range" min="0" max="500" value="' + this.constants.physics.hierarchicalRepulsion.springLength + '" step="1" style="width:300px" id="graph_H_sl"></td><td>500</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.springLength + '" id="graph_H_sl_value" style="width:60px"></td>' + | |||||
| '</tr>' + | |||||
| '<tr>' + | |||||
| '<td width="150px">springConstant</td><td>0</td><td><input type="range" min="0" max="0.5" value="' + this.constants.physics.hierarchicalRepulsion.springConstant + '" step="0.001" style="width:300px" id="graph_H_sc"></td><td>0.5</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.springConstant + '" id="graph_H_sc_value" style="width:60px"></td>' + | |||||
| '</tr>' + | |||||
| '<tr>' + | |||||
| '<td width="150px">damping</td><td>0</td><td><input type="range" min="0" max="0.3" value="' + this.constants.physics.hierarchicalRepulsion.damping + '" step="0.005" style="width:300px" id="graph_H_damp"></td><td>0.3</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.damping + '" id="graph_H_damp_value" style="width:60px"></td>' + | |||||
| '</tr>' + | |||||
| '<tr>' + | |||||
| '<td width="150px">direction</td><td>1</td><td><input type="range" min="0" max="3" value="' + hierarchicalLayoutDirections.indexOf(this.constants.hierarchicalLayout.direction) + '" step="1" style="width:300px" id="graph_H_direction"></td><td>4</td><td><input value="' + this.constants.hierarchicalLayout.direction + '" id="graph_H_direction_value" style="width:60px"></td>' + | |||||
| '</tr>' + | |||||
| '<tr>' + | |||||
| '<td width="150px">levelSeparation</td><td>1</td><td><input type="range" min="0" max="500" value="' + this.constants.hierarchicalLayout.levelSeparation + '" step="1" style="width:300px" id="graph_H_levsep"></td><td>500</td><td><input value="' + this.constants.hierarchicalLayout.levelSeparation + '" id="graph_H_levsep_value" style="width:60px"></td>' + | |||||
| '</tr>' + | |||||
| '<tr>' + | |||||
| '<td width="150px">nodeSpacing</td><td>1</td><td><input type="range" min="0" max="500" value="' + this.constants.hierarchicalLayout.nodeSpacing + '" step="1" style="width:300px" id="graph_H_nspac"></td><td>500</td><td><input value="' + this.constants.hierarchicalLayout.nodeSpacing + '" id="graph_H_nspac_value" style="width:60px"></td>' + | |||||
| '</tr>' + | |||||
| '</table>' + | |||||
| '<table><tr><td><b>Options:</b></td></tr>' + | |||||
| '<tr>' + | |||||
| '<td width="180px"><input type="button" id="graph_toggleSmooth" value="Toggle smoothCurves" style="width:150px"></td>' + | |||||
| '<td width="180px"><input type="button" id="graph_repositionNodes" value="Reinitialize" style="width:150px"></td>' + | |||||
| '<td width="180px"><input type="button" id="graph_generateOptions" value="Generate Options" style="width:150px"></td>' + | |||||
| '</tr>' + | |||||
| '</table>' | |||||
| this.containerElement.parentElement.insertBefore(this.physicsConfiguration, this.containerElement); | |||||
| this.optionsDiv = document.createElement("div"); | |||||
| this.optionsDiv.style.fontSize = "14px"; | |||||
| this.optionsDiv.style.fontFamily = "verdana"; | |||||
| this.containerElement.parentElement.insertBefore(this.optionsDiv, this.containerElement); | |||||
| var rangeElement; | |||||
| rangeElement = document.getElementById('graph_BH_gc'); | |||||
| rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_gc', -1, "physics_barnesHut_gravitationalConstant"); | |||||
| rangeElement = document.getElementById('graph_BH_cg'); | |||||
| rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_cg', 1, "physics_centralGravity"); | |||||
| rangeElement = document.getElementById('graph_BH_sc'); | |||||
| rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sc', 1, "physics_springConstant"); | |||||
| rangeElement = document.getElementById('graph_BH_sl'); | |||||
| rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sl', 1, "physics_springLength"); | |||||
| rangeElement = document.getElementById('graph_BH_damp'); | |||||
| rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_damp', 1, "physics_damping"); | |||||
| rangeElement = document.getElementById('graph_R_nd'); | |||||
| rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_nd', 1, "physics_repulsion_nodeDistance"); | |||||
| rangeElement = document.getElementById('graph_R_cg'); | |||||
| rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_cg', 1, "physics_centralGravity"); | |||||
| rangeElement = document.getElementById('graph_R_sc'); | |||||
| rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sc', 1, "physics_springConstant"); | |||||
| rangeElement = document.getElementById('graph_R_sl'); | |||||
| rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sl', 1, "physics_springLength"); | |||||
| rangeElement = document.getElementById('graph_R_damp'); | |||||
| rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_damp', 1, "physics_damping"); | |||||
| rangeElement = document.getElementById('graph_H_nd'); | |||||
| rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nd', 1, "physics_hierarchicalRepulsion_nodeDistance"); | |||||
| rangeElement = document.getElementById('graph_H_cg'); | |||||
| rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_cg', 1, "physics_centralGravity"); | |||||
| rangeElement = document.getElementById('graph_H_sc'); | |||||
| rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sc', 1, "physics_springConstant"); | |||||
| rangeElement = document.getElementById('graph_H_sl'); | |||||
| rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sl', 1, "physics_springLength"); | |||||
| rangeElement = document.getElementById('graph_H_damp'); | |||||
| rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_damp', 1, "physics_damping"); | |||||
| rangeElement = document.getElementById('graph_H_direction'); | |||||
| rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_direction', hierarchicalLayoutDirections, "hierarchicalLayout_direction"); | |||||
| rangeElement = document.getElementById('graph_H_levsep'); | |||||
| rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_levsep', 1, "hierarchicalLayout_levelSeparation"); | |||||
| rangeElement = document.getElementById('graph_H_nspac'); | |||||
| rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nspac', 1, "hierarchicalLayout_nodeSpacing"); | |||||
| var radioButton1 = document.getElementById("graph_physicsMethod1"); | |||||
| var radioButton2 = document.getElementById("graph_physicsMethod2"); | |||||
| var radioButton3 = document.getElementById("graph_physicsMethod3"); | |||||
| radioButton2.checked = true; | |||||
| if (this.constants.physics.barnesHut.enabled) { | |||||
| radioButton1.checked = true; | |||||
| } | |||||
| if (this.constants.hierarchicalLayout.enabled) { | |||||
| radioButton3.checked = true; | |||||
| } | |||||
| var graph_toggleSmooth = document.getElementById("graph_toggleSmooth"); | |||||
| var graph_repositionNodes = document.getElementById("graph_repositionNodes"); | |||||
| var graph_generateOptions = document.getElementById("graph_generateOptions"); | |||||
| graph_toggleSmooth.onclick = graphToggleSmoothCurves.bind(this); | |||||
| graph_repositionNodes.onclick = graphRepositionNodes.bind(this); | |||||
| graph_generateOptions.onclick = graphGenerateOptions.bind(this); | |||||
| if (this.constants.smoothCurves == true) { | |||||
| graph_toggleSmooth.style.background = "#A4FF56"; | |||||
| } | |||||
| else { | |||||
| graph_toggleSmooth.style.background = "#FF8532"; | |||||
| } | |||||
| switchConfigurations.apply(this); | |||||
| radioButton1.onchange = switchConfigurations.bind(this); | |||||
| radioButton2.onchange = switchConfigurations.bind(this); | |||||
| radioButton3.onchange = switchConfigurations.bind(this); | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * This overwrites the this.constants. | |||||
| * | |||||
| * @param constantsVariableName | |||||
| * @param value | |||||
| * @private | |||||
| */ | |||||
| exports._overWriteGraphConstants = function (constantsVariableName, value) { | |||||
| var nameArray = constantsVariableName.split("_"); | |||||
| if (nameArray.length == 1) { | |||||
| this.constants[nameArray[0]] = value; | |||||
| } | |||||
| else if (nameArray.length == 2) { | |||||
| this.constants[nameArray[0]][nameArray[1]] = value; | |||||
| } | |||||
| else if (nameArray.length == 3) { | |||||
| this.constants[nameArray[0]][nameArray[1]][nameArray[2]] = value; | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * this function is bound to the toggle smooth curves button. That is also why it is not in the prototype. | |||||
| */ | |||||
| function graphToggleSmoothCurves () { | |||||
| this.constants.smoothCurves = !this.constants.smoothCurves; | |||||
| var graph_toggleSmooth = document.getElementById("graph_toggleSmooth"); | |||||
| if (this.constants.smoothCurves == true) {graph_toggleSmooth.style.background = "#A4FF56";} | |||||
| else {graph_toggleSmooth.style.background = "#FF8532";} | |||||
| this._configureSmoothCurves(false); | |||||
| } | |||||
| /** | |||||
| * this function is used to scramble the nodes | |||||
| * | |||||
| */ | |||||
| function graphRepositionNodes () { | |||||
| for (var nodeId in this.calculationNodes) { | |||||
| if (this.calculationNodes.hasOwnProperty(nodeId)) { | |||||
| this.calculationNodes[nodeId].vx = 0; this.calculationNodes[nodeId].vy = 0; | |||||
| this.calculationNodes[nodeId].fx = 0; this.calculationNodes[nodeId].fy = 0; | |||||
| } | |||||
| } | |||||
| if (this.constants.hierarchicalLayout.enabled == true) { | |||||
| this._setupHierarchicalLayout(); | |||||
| } | |||||
| else { | |||||
| this.repositionNodes(); | |||||
| } | |||||
| this.moving = true; | |||||
| this.start(); | |||||
| } | |||||
| /** | |||||
| * this is used to generate an options file from the playing with physics system. | |||||
| */ | |||||
| function graphGenerateOptions () { | |||||
| var options = "No options are required, default values used."; | |||||
| var optionsSpecific = []; | |||||
| var radioButton1 = document.getElementById("graph_physicsMethod1"); | |||||
| var radioButton2 = document.getElementById("graph_physicsMethod2"); | |||||
| if (radioButton1.checked == true) { | |||||
| if (this.constants.physics.barnesHut.gravitationalConstant != this.backupConstants.physics.barnesHut.gravitationalConstant) {optionsSpecific.push("gravitationalConstant: " + this.constants.physics.barnesHut.gravitationalConstant);} | |||||
| if (this.constants.physics.centralGravity != this.backupConstants.physics.barnesHut.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);} | |||||
| if (this.constants.physics.springLength != this.backupConstants.physics.barnesHut.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);} | |||||
| if (this.constants.physics.springConstant != this.backupConstants.physics.barnesHut.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);} | |||||
| if (this.constants.physics.damping != this.backupConstants.physics.barnesHut.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);} | |||||
| if (optionsSpecific.length != 0) { | |||||
| options = "var options = {"; | |||||
| options += "physics: {barnesHut: {"; | |||||
| for (var i = 0; i < optionsSpecific.length; i++) { | |||||
| options += optionsSpecific[i]; | |||||
| if (i < optionsSpecific.length - 1) { | |||||
| options += ", " | |||||
| } | |||||
| } | |||||
| options += '}}' | |||||
| } | |||||
| if (this.constants.smoothCurves != this.backupConstants.smoothCurves) { | |||||
| if (optionsSpecific.length == 0) {options = "var options = {";} | |||||
| else {options += ", "} | |||||
| options += "smoothCurves: " + this.constants.smoothCurves; | |||||
| } | |||||
| if (options != "No options are required, default values used.") { | |||||
| options += '};' | |||||
| } | |||||
| } | |||||
| else if (radioButton2.checked == true) { | |||||
| options = "var options = {"; | |||||
| options += "physics: {barnesHut: {enabled: false}"; | |||||
| if (this.constants.physics.repulsion.nodeDistance != this.backupConstants.physics.repulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.repulsion.nodeDistance);} | |||||
| if (this.constants.physics.centralGravity != this.backupConstants.physics.repulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);} | |||||
| if (this.constants.physics.springLength != this.backupConstants.physics.repulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);} | |||||
| if (this.constants.physics.springConstant != this.backupConstants.physics.repulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);} | |||||
| if (this.constants.physics.damping != this.backupConstants.physics.repulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);} | |||||
| if (optionsSpecific.length != 0) { | |||||
| options += ", repulsion: {"; | |||||
| for (var i = 0; i < optionsSpecific.length; i++) { | |||||
| options += optionsSpecific[i]; | |||||
| if (i < optionsSpecific.length - 1) { | |||||
| options += ", " | |||||
| } | |||||
| } | |||||
| options += '}}' | |||||
| } | |||||
| if (optionsSpecific.length == 0) {options += "}"} | |||||
| if (this.constants.smoothCurves != this.backupConstants.smoothCurves) { | |||||
| options += ", smoothCurves: " + this.constants.smoothCurves; | |||||
| } | |||||
| options += '};' | |||||
| } | |||||
| else { | |||||
| options = "var options = {"; | |||||
| if (this.constants.physics.hierarchicalRepulsion.nodeDistance != this.backupConstants.physics.hierarchicalRepulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.hierarchicalRepulsion.nodeDistance);} | |||||
| if (this.constants.physics.centralGravity != this.backupConstants.physics.hierarchicalRepulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);} | |||||
| if (this.constants.physics.springLength != this.backupConstants.physics.hierarchicalRepulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);} | |||||
| if (this.constants.physics.springConstant != this.backupConstants.physics.hierarchicalRepulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);} | |||||
| if (this.constants.physics.damping != this.backupConstants.physics.hierarchicalRepulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);} | |||||
| if (optionsSpecific.length != 0) { | |||||
| options += "physics: {hierarchicalRepulsion: {"; | |||||
| for (var i = 0; i < optionsSpecific.length; i++) { | |||||
| options += optionsSpecific[i]; | |||||
| if (i < optionsSpecific.length - 1) { | |||||
| options += ", "; | |||||
| } | |||||
| } | |||||
| options += '}},'; | |||||
| } | |||||
| options += 'hierarchicalLayout: {'; | |||||
| optionsSpecific = []; | |||||
| if (this.constants.hierarchicalLayout.direction != this.backupConstants.hierarchicalLayout.direction) {optionsSpecific.push("direction: " + this.constants.hierarchicalLayout.direction);} | |||||
| if (Math.abs(this.constants.hierarchicalLayout.levelSeparation) != this.backupConstants.hierarchicalLayout.levelSeparation) {optionsSpecific.push("levelSeparation: " + this.constants.hierarchicalLayout.levelSeparation);} | |||||
| if (this.constants.hierarchicalLayout.nodeSpacing != this.backupConstants.hierarchicalLayout.nodeSpacing) {optionsSpecific.push("nodeSpacing: " + this.constants.hierarchicalLayout.nodeSpacing);} | |||||
| if (optionsSpecific.length != 0) { | |||||
| for (var i = 0; i < optionsSpecific.length; i++) { | |||||
| options += optionsSpecific[i]; | |||||
| if (i < optionsSpecific.length - 1) { | |||||
| options += ", " | |||||
| } | |||||
| } | |||||
| options += '}' | |||||
| } | |||||
| else { | |||||
| options += "enabled:true}"; | |||||
| } | |||||
| options += '};' | |||||
| } | |||||
| this.optionsDiv.innerHTML = options; | |||||
| } | |||||
| /** | |||||
| * this is used to switch between barnesHut, repulsion and hierarchical. | |||||
| * | |||||
| */ | |||||
| function switchConfigurations () { | |||||
| var ids = ["graph_BH_table", "graph_R_table", "graph_H_table"]; | |||||
| var radioButton = document.querySelector('input[name="graph_physicsMethod"]:checked').value; | |||||
| var tableId = "graph_" + radioButton + "_table"; | |||||
| var table = document.getElementById(tableId); | |||||
| table.style.display = "block"; | |||||
| for (var i = 0; i < ids.length; i++) { | |||||
| if (ids[i] != tableId) { | |||||
| table = document.getElementById(ids[i]); | |||||
| table.style.display = "none"; | |||||
| } | |||||
| } | |||||
| this._restoreNodes(); | |||||
| if (radioButton == "R") { | |||||
| this.constants.hierarchicalLayout.enabled = false; | |||||
| this.constants.physics.hierarchicalRepulsion.enabled = false; | |||||
| this.constants.physics.barnesHut.enabled = false; | |||||
| } | |||||
| else if (radioButton == "H") { | |||||
| if (this.constants.hierarchicalLayout.enabled == false) { | |||||
| this.constants.hierarchicalLayout.enabled = true; | |||||
| this.constants.physics.hierarchicalRepulsion.enabled = true; | |||||
| this.constants.physics.barnesHut.enabled = false; | |||||
| this._setupHierarchicalLayout(); | |||||
| } | |||||
| } | |||||
| else { | |||||
| this.constants.hierarchicalLayout.enabled = false; | |||||
| this.constants.physics.hierarchicalRepulsion.enabled = false; | |||||
| this.constants.physics.barnesHut.enabled = true; | |||||
| } | |||||
| this._loadSelectedForceSolver(); | |||||
| var graph_toggleSmooth = document.getElementById("graph_toggleSmooth"); | |||||
| if (this.constants.smoothCurves == true) {graph_toggleSmooth.style.background = "#A4FF56";} | |||||
| else {graph_toggleSmooth.style.background = "#FF8532";} | |||||
| this.moving = true; | |||||
| this.start(); | |||||
| } | |||||
| /** | |||||
| * this generates the ranges depending on the iniital values. | |||||
| * | |||||
| * @param id | |||||
| * @param map | |||||
| * @param constantsVariableName | |||||
| */ | |||||
| function showValueOfRange (id,map,constantsVariableName) { | |||||
| var valueId = id + "_value"; | |||||
| var rangeValue = document.getElementById(id).value; | |||||
| if (map instanceof Array) { | |||||
| document.getElementById(valueId).value = map[parseInt(rangeValue)]; | |||||
| this._overWriteGraphConstants(constantsVariableName,map[parseInt(rangeValue)]); | |||||
| } | |||||
| else { | |||||
| document.getElementById(valueId).value = parseInt(map) * parseFloat(rangeValue); | |||||
| this._overWriteGraphConstants(constantsVariableName, parseInt(map) * parseFloat(rangeValue)); | |||||
| } | |||||
| if (constantsVariableName == "hierarchicalLayout_direction" || | |||||
| constantsVariableName == "hierarchicalLayout_levelSeparation" || | |||||
| constantsVariableName == "hierarchicalLayout_nodeSpacing") { | |||||
| this._setupHierarchicalLayout(); | |||||
| } | |||||
| this.moving = true; | |||||
| this.start(); | |||||
| } | |||||
| @ -0,0 +1,58 @@ | |||||
| /** | |||||
| * Calculate the forces the nodes apply on each other based on a repulsion field. | |||||
| * This field is linearly approximated. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._calculateNodeForces = function () { | |||||
| var dx, dy, angle, distance, fx, fy, combinedClusterSize, | |||||
| repulsingForce, node1, node2, i, j; | |||||
| var nodes = this.calculationNodes; | |||||
| var nodeIndices = this.calculationNodeIndices; | |||||
| // approximation constants | |||||
| var a_base = -2 / 3; | |||||
| var b = 4 / 3; | |||||
| // repulsing forces between nodes | |||||
| var nodeDistance = this.constants.physics.repulsion.nodeDistance; | |||||
| var minimumDistance = nodeDistance; | |||||
| // we loop from i over all but the last entree in the array | |||||
| // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j | |||||
| for (i = 0; i < nodeIndices.length - 1; i++) { | |||||
| node1 = nodes[nodeIndices[i]]; | |||||
| for (j = i + 1; j < nodeIndices.length; j++) { | |||||
| node2 = nodes[nodeIndices[j]]; | |||||
| combinedClusterSize = node1.clusterSize + node2.clusterSize - 2; | |||||
| dx = node2.x - node1.x; | |||||
| dy = node2.y - node1.y; | |||||
| distance = Math.sqrt(dx * dx + dy * dy); | |||||
| minimumDistance = (combinedClusterSize == 0) ? nodeDistance : (nodeDistance * (1 + combinedClusterSize * this.constants.clustering.distanceAmplification)); | |||||
| var a = a_base / minimumDistance; | |||||
| if (distance < 2 * minimumDistance) { | |||||
| if (distance < 0.5 * minimumDistance) { | |||||
| repulsingForce = 1.0; | |||||
| } | |||||
| else { | |||||
| repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)) | |||||
| } | |||||
| // amplify the repulsion for clusters. | |||||
| repulsingForce *= (combinedClusterSize == 0) ? 1 : 1 + combinedClusterSize * this.constants.clustering.forceAmplification; | |||||
| repulsingForce = repulsingForce / distance; | |||||
| fx = dx * repulsingForce; | |||||
| fy = dy * repulsingForce; | |||||
| node1.fx -= fx; | |||||
| node1.fy -= fy; | |||||
| node2.fx += fx; | |||||
| node2.fy += fy; | |||||
| } | |||||
| } | |||||
| } | |||||
| }; | |||||
| @ -1,84 +0,0 @@ | |||||
| /** | |||||
| * vis.js module exports | |||||
| */ | |||||
| var vis = { | |||||
| moment: moment, | |||||
| util: util, | |||||
| DOMutil: DOMutil, | |||||
| DataSet: DataSet, | |||||
| DataView: DataView, | |||||
| Timeline: Timeline, | |||||
| Graph2d: Graph2d, | |||||
| timeline: { | |||||
| DataStep: DataStep, | |||||
| Range: Range, | |||||
| stack: stack, | |||||
| TimeStep: TimeStep, | |||||
| components: { | |||||
| items: { | |||||
| Item: Item, | |||||
| ItemBox: ItemBox, | |||||
| ItemPoint: ItemPoint, | |||||
| ItemRange: ItemRange | |||||
| }, | |||||
| Component: Component, | |||||
| CurrentTime: CurrentTime, | |||||
| CustomTime: CustomTime, | |||||
| DataAxis: DataAxis, | |||||
| GraphGroup: GraphGroup, | |||||
| Group: Group, | |||||
| ItemSet: ItemSet, | |||||
| Legend: Legend, | |||||
| LineGraph: LineGraph, | |||||
| TimeAxis: TimeAxis | |||||
| } | |||||
| }, | |||||
| Network: Network, | |||||
| network: { | |||||
| Edge: Edge, | |||||
| Groups: Groups, | |||||
| Images: Images, | |||||
| Node: Node, | |||||
| Popup: Popup | |||||
| }, | |||||
| // Deprecated since v3.0.0 | |||||
| Graph: function () { | |||||
| throw new Error('Graph is renamed to Network. Please create a graph as new vis.Network(...)'); | |||||
| }, | |||||
| Graph3d: Graph3d | |||||
| }; | |||||
| /** | |||||
| * CommonJS module exports | |||||
| */ | |||||
| if (typeof exports !== 'undefined') { | |||||
| exports = vis; | |||||
| } | |||||
| if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { | |||||
| module.exports = vis; | |||||
| } | |||||
| /** | |||||
| * AMD module exports | |||||
| */ | |||||
| if (typeof(define) === 'function') { | |||||
| define(function () { | |||||
| return vis; | |||||
| }); | |||||
| } | |||||
| /** | |||||
| * Window exports | |||||
| */ | |||||
| if (typeof window !== 'undefined') { | |||||
| // attach the module to the window, load as a regular javascript file | |||||
| window['vis'] = vis; | |||||
| } | |||||
| @ -1,31 +0,0 @@ | |||||
| /** | |||||
| * vis.js module imports | |||||
| */ | |||||
| // Try to load dependencies from the global window object. | |||||
| // If not available there, load via require. | |||||
| var moment = (typeof window !== 'undefined') && window['moment'] || require('moment'); | |||||
| var Emitter = require('emitter-component'); | |||||
| var Hammer; | |||||
| if (typeof window !== 'undefined') { | |||||
| // load hammer.js only when running in a browser (where window is available) | |||||
| Hammer = window['Hammer'] || require('hammerjs'); | |||||
| } | |||||
| else { | |||||
| Hammer = function () { | |||||
| throw Error('hammer.js is only available in a browser, not in node.js.'); | |||||
| } | |||||
| } | |||||
| var mousetrap; | |||||
| if (typeof window !== 'undefined') { | |||||
| // load mousetrap.js only when running in a browser (where window is available) | |||||
| mousetrap = window['mousetrap'] || require('mousetrap'); | |||||
| } | |||||
| else { | |||||
| mousetrap = function () { | |||||
| throw Error('mouseTrap is only available in a browser, not in node.js.'); | |||||
| } | |||||
| } | |||||
| @ -1,829 +0,0 @@ | |||||
| (function(exports) { | |||||
| /** | |||||
| * Parse a text source containing data in DOT language into a JSON object. | |||||
| * The object contains two lists: one with nodes and one with edges. | |||||
| * | |||||
| * DOT language reference: http://www.graphviz.org/doc/info/lang.html | |||||
| * | |||||
| * @param {String} data Text containing a graph in DOT-notation | |||||
| * @return {Object} graph An object containing two parameters: | |||||
| * {Object[]} nodes | |||||
| * {Object[]} edges | |||||
| */ | |||||
| function parseDOT (data) { | |||||
| dot = data; | |||||
| return parseGraph(); | |||||
| } | |||||
| // token types enumeration | |||||
| var TOKENTYPE = { | |||||
| NULL : 0, | |||||
| DELIMITER : 1, | |||||
| IDENTIFIER: 2, | |||||
| UNKNOWN : 3 | |||||
| }; | |||||
| // map with all delimiters | |||||
| var DELIMITERS = { | |||||
| '{': true, | |||||
| '}': true, | |||||
| '[': true, | |||||
| ']': true, | |||||
| ';': true, | |||||
| '=': true, | |||||
| ',': true, | |||||
| '->': true, | |||||
| '--': true | |||||
| }; | |||||
| var dot = ''; // current dot file | |||||
| var index = 0; // current index in dot file | |||||
| var c = ''; // current token character in expr | |||||
| var token = ''; // current token | |||||
| var tokenType = TOKENTYPE.NULL; // type of the token | |||||
| /** | |||||
| * Get the first character from the dot file. | |||||
| * The character is stored into the char c. If the end of the dot file is | |||||
| * reached, the function puts an empty string in c. | |||||
| */ | |||||
| function first() { | |||||
| index = 0; | |||||
| c = dot.charAt(0); | |||||
| } | |||||
| /** | |||||
| * Get the next character from the dot file. | |||||
| * The character is stored into the char c. If the end of the dot file is | |||||
| * reached, the function puts an empty string in c. | |||||
| */ | |||||
| function next() { | |||||
| index++; | |||||
| c = dot.charAt(index); | |||||
| } | |||||
| /** | |||||
| * Preview the next character from the dot file. | |||||
| * @return {String} cNext | |||||
| */ | |||||
| function nextPreview() { | |||||
| return dot.charAt(index + 1); | |||||
| } | |||||
| /** | |||||
| * Test whether given character is alphabetic or numeric | |||||
| * @param {String} c | |||||
| * @return {Boolean} isAlphaNumeric | |||||
| */ | |||||
| var regexAlphaNumeric = /[a-zA-Z_0-9.:#]/; | |||||
| function isAlphaNumeric(c) { | |||||
| return regexAlphaNumeric.test(c); | |||||
| } | |||||
| /** | |||||
| * Merge all properties of object b into object b | |||||
| * @param {Object} a | |||||
| * @param {Object} b | |||||
| * @return {Object} a | |||||
| */ | |||||
| function merge (a, b) { | |||||
| if (!a) { | |||||
| a = {}; | |||||
| } | |||||
| if (b) { | |||||
| for (var name in b) { | |||||
| if (b.hasOwnProperty(name)) { | |||||
| a[name] = b[name]; | |||||
| } | |||||
| } | |||||
| } | |||||
| return a; | |||||
| } | |||||
| /** | |||||
| * Set a value in an object, where the provided parameter name can be a | |||||
| * path with nested parameters. For example: | |||||
| * | |||||
| * var obj = {a: 2}; | |||||
| * setValue(obj, 'b.c', 3); // obj = {a: 2, b: {c: 3}} | |||||
| * | |||||
| * @param {Object} obj | |||||
| * @param {String} path A parameter name or dot-separated parameter path, | |||||
| * like "color.highlight.border". | |||||
| * @param {*} value | |||||
| */ | |||||
| function setValue(obj, path, value) { | |||||
| var keys = path.split('.'); | |||||
| var o = obj; | |||||
| while (keys.length) { | |||||
| var key = keys.shift(); | |||||
| if (keys.length) { | |||||
| // this isn't the end point | |||||
| if (!o[key]) { | |||||
| o[key] = {}; | |||||
| } | |||||
| o = o[key]; | |||||
| } | |||||
| else { | |||||
| // this is the end point | |||||
| o[key] = value; | |||||
| } | |||||
| } | |||||
| } | |||||
| /** | |||||
| * Add a node to a graph object. If there is already a node with | |||||
| * the same id, their attributes will be merged. | |||||
| * @param {Object} graph | |||||
| * @param {Object} node | |||||
| */ | |||||
| function addNode(graph, node) { | |||||
| var i, len; | |||||
| var current = null; | |||||
| // find root graph (in case of subgraph) | |||||
| var graphs = [graph]; // list with all graphs from current graph to root graph | |||||
| var root = graph; | |||||
| while (root.parent) { | |||||
| graphs.push(root.parent); | |||||
| root = root.parent; | |||||
| } | |||||
| // find existing node (at root level) by its id | |||||
| if (root.nodes) { | |||||
| for (i = 0, len = root.nodes.length; i < len; i++) { | |||||
| if (node.id === root.nodes[i].id) { | |||||
| current = root.nodes[i]; | |||||
| break; | |||||
| } | |||||
| } | |||||
| } | |||||
| if (!current) { | |||||
| // this is a new node | |||||
| current = { | |||||
| id: node.id | |||||
| }; | |||||
| if (graph.node) { | |||||
| // clone default attributes | |||||
| current.attr = merge(current.attr, graph.node); | |||||
| } | |||||
| } | |||||
| // add node to this (sub)graph and all its parent graphs | |||||
| for (i = graphs.length - 1; i >= 0; i--) { | |||||
| var g = graphs[i]; | |||||
| if (!g.nodes) { | |||||
| g.nodes = []; | |||||
| } | |||||
| if (g.nodes.indexOf(current) == -1) { | |||||
| g.nodes.push(current); | |||||
| } | |||||
| } | |||||
| // merge attributes | |||||
| if (node.attr) { | |||||
| current.attr = merge(current.attr, node.attr); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * Add an edge to a graph object | |||||
| * @param {Object} graph | |||||
| * @param {Object} edge | |||||
| */ | |||||
| function addEdge(graph, edge) { | |||||
| if (!graph.edges) { | |||||
| graph.edges = []; | |||||
| } | |||||
| graph.edges.push(edge); | |||||
| if (graph.edge) { | |||||
| var attr = merge({}, graph.edge); // clone default attributes | |||||
| edge.attr = merge(attr, edge.attr); // merge attributes | |||||
| } | |||||
| } | |||||
| /** | |||||
| * Create an edge to a graph object | |||||
| * @param {Object} graph | |||||
| * @param {String | Number | Object} from | |||||
| * @param {String | Number | Object} to | |||||
| * @param {String} type | |||||
| * @param {Object | null} attr | |||||
| * @return {Object} edge | |||||
| */ | |||||
| function createEdge(graph, from, to, type, attr) { | |||||
| var edge = { | |||||
| from: from, | |||||
| to: to, | |||||
| type: type | |||||
| }; | |||||
| if (graph.edge) { | |||||
| edge.attr = merge({}, graph.edge); // clone default attributes | |||||
| } | |||||
| edge.attr = merge(edge.attr || {}, attr); // merge attributes | |||||
| return edge; | |||||
| } | |||||
| /** | |||||
| * Get next token in the current dot file. | |||||
| * The token and token type are available as token and tokenType | |||||
| */ | |||||
| function getToken() { | |||||
| tokenType = TOKENTYPE.NULL; | |||||
| token = ''; | |||||
| // skip over whitespaces | |||||
| while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter | |||||
| next(); | |||||
| } | |||||
| do { | |||||
| var isComment = false; | |||||
| // skip comment | |||||
| if (c == '#') { | |||||
| // find the previous non-space character | |||||
| var i = index - 1; | |||||
| while (dot.charAt(i) == ' ' || dot.charAt(i) == '\t') { | |||||
| i--; | |||||
| } | |||||
| if (dot.charAt(i) == '\n' || dot.charAt(i) == '') { | |||||
| // the # is at the start of a line, this is indeed a line comment | |||||
| while (c != '' && c != '\n') { | |||||
| next(); | |||||
| } | |||||
| isComment = true; | |||||
| } | |||||
| } | |||||
| if (c == '/' && nextPreview() == '/') { | |||||
| // skip line comment | |||||
| while (c != '' && c != '\n') { | |||||
| next(); | |||||
| } | |||||
| isComment = true; | |||||
| } | |||||
| if (c == '/' && nextPreview() == '*') { | |||||
| // skip block comment | |||||
| while (c != '') { | |||||
| if (c == '*' && nextPreview() == '/') { | |||||
| // end of block comment found. skip these last two characters | |||||
| next(); | |||||
| next(); | |||||
| break; | |||||
| } | |||||
| else { | |||||
| next(); | |||||
| } | |||||
| } | |||||
| isComment = true; | |||||
| } | |||||
| // skip over whitespaces | |||||
| while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter | |||||
| next(); | |||||
| } | |||||
| } | |||||
| while (isComment); | |||||
| // check for end of dot file | |||||
| if (c == '') { | |||||
| // token is still empty | |||||
| tokenType = TOKENTYPE.DELIMITER; | |||||
| return; | |||||
| } | |||||
| // check for delimiters consisting of 2 characters | |||||
| var c2 = c + nextPreview(); | |||||
| if (DELIMITERS[c2]) { | |||||
| tokenType = TOKENTYPE.DELIMITER; | |||||
| token = c2; | |||||
| next(); | |||||
| next(); | |||||
| return; | |||||
| } | |||||
| // check for delimiters consisting of 1 character | |||||
| if (DELIMITERS[c]) { | |||||
| tokenType = TOKENTYPE.DELIMITER; | |||||
| token = c; | |||||
| next(); | |||||
| return; | |||||
| } | |||||
| // check for an identifier (number or string) | |||||
| // TODO: more precise parsing of numbers/strings (and the port separator ':') | |||||
| if (isAlphaNumeric(c) || c == '-') { | |||||
| token += c; | |||||
| next(); | |||||
| while (isAlphaNumeric(c)) { | |||||
| token += c; | |||||
| next(); | |||||
| } | |||||
| if (token == 'false') { | |||||
| token = false; // convert to boolean | |||||
| } | |||||
| else if (token == 'true') { | |||||
| token = true; // convert to boolean | |||||
| } | |||||
| else if (!isNaN(Number(token))) { | |||||
| token = Number(token); // convert to number | |||||
| } | |||||
| tokenType = TOKENTYPE.IDENTIFIER; | |||||
| return; | |||||
| } | |||||
| // check for a string enclosed by double quotes | |||||
| if (c == '"') { | |||||
| next(); | |||||
| while (c != '' && (c != '"' || (c == '"' && nextPreview() == '"'))) { | |||||
| token += c; | |||||
| if (c == '"') { // skip the escape character | |||||
| next(); | |||||
| } | |||||
| next(); | |||||
| } | |||||
| if (c != '"') { | |||||
| throw newSyntaxError('End of string " expected'); | |||||
| } | |||||
| next(); | |||||
| tokenType = TOKENTYPE.IDENTIFIER; | |||||
| return; | |||||
| } | |||||
| // something unknown is found, wrong characters, a syntax error | |||||
| tokenType = TOKENTYPE.UNKNOWN; | |||||
| while (c != '') { | |||||
| token += c; | |||||
| next(); | |||||
| } | |||||
| throw new SyntaxError('Syntax error in part "' + chop(token, 30) + '"'); | |||||
| } | |||||
| /** | |||||
| * Parse a graph. | |||||
| * @returns {Object} graph | |||||
| */ | |||||
| function parseGraph() { | |||||
| var graph = {}; | |||||
| first(); | |||||
| getToken(); | |||||
| // optional strict keyword | |||||
| if (token == 'strict') { | |||||
| graph.strict = true; | |||||
| getToken(); | |||||
| } | |||||
| // graph or digraph keyword | |||||
| if (token == 'graph' || token == 'digraph') { | |||||
| graph.type = token; | |||||
| getToken(); | |||||
| } | |||||
| // optional graph id | |||||
| if (tokenType == TOKENTYPE.IDENTIFIER) { | |||||
| graph.id = token; | |||||
| getToken(); | |||||
| } | |||||
| // open angle bracket | |||||
| if (token != '{') { | |||||
| throw newSyntaxError('Angle bracket { expected'); | |||||
| } | |||||
| getToken(); | |||||
| // statements | |||||
| parseStatements(graph); | |||||
| // close angle bracket | |||||
| if (token != '}') { | |||||
| throw newSyntaxError('Angle bracket } expected'); | |||||
| } | |||||
| getToken(); | |||||
| // end of file | |||||
| if (token !== '') { | |||||
| throw newSyntaxError('End of file expected'); | |||||
| } | |||||
| getToken(); | |||||
| // remove temporary default properties | |||||
| delete graph.node; | |||||
| delete graph.edge; | |||||
| delete graph.graph; | |||||
| return graph; | |||||
| } | |||||
| /** | |||||
| * Parse a list with statements. | |||||
| * @param {Object} graph | |||||
| */ | |||||
| function parseStatements (graph) { | |||||
| while (token !== '' && token != '}') { | |||||
| parseStatement(graph); | |||||
| if (token == ';') { | |||||
| getToken(); | |||||
| } | |||||
| } | |||||
| } | |||||
| /** | |||||
| * Parse a single statement. Can be a an attribute statement, node | |||||
| * statement, a series of node statements and edge statements, or a | |||||
| * parameter. | |||||
| * @param {Object} graph | |||||
| */ | |||||
| function parseStatement(graph) { | |||||
| // parse subgraph | |||||
| var subgraph = parseSubgraph(graph); | |||||
| if (subgraph) { | |||||
| // edge statements | |||||
| parseEdge(graph, subgraph); | |||||
| return; | |||||
| } | |||||
| // parse an attribute statement | |||||
| var attr = parseAttributeStatement(graph); | |||||
| if (attr) { | |||||
| return; | |||||
| } | |||||
| // parse node | |||||
| if (tokenType != TOKENTYPE.IDENTIFIER) { | |||||
| throw newSyntaxError('Identifier expected'); | |||||
| } | |||||
| var id = token; // id can be a string or a number | |||||
| getToken(); | |||||
| if (token == '=') { | |||||
| // id statement | |||||
| getToken(); | |||||
| if (tokenType != TOKENTYPE.IDENTIFIER) { | |||||
| throw newSyntaxError('Identifier expected'); | |||||
| } | |||||
| graph[id] = token; | |||||
| getToken(); | |||||
| // TODO: implement comma separated list with "a_list: ID=ID [','] [a_list] " | |||||
| } | |||||
| else { | |||||
| parseNodeStatement(graph, id); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * Parse a subgraph | |||||
| * @param {Object} graph parent graph object | |||||
| * @return {Object | null} subgraph | |||||
| */ | |||||
| function parseSubgraph (graph) { | |||||
| var subgraph = null; | |||||
| // optional subgraph keyword | |||||
| if (token == 'subgraph') { | |||||
| subgraph = {}; | |||||
| subgraph.type = 'subgraph'; | |||||
| getToken(); | |||||
| // optional graph id | |||||
| if (tokenType == TOKENTYPE.IDENTIFIER) { | |||||
| subgraph.id = token; | |||||
| getToken(); | |||||
| } | |||||
| } | |||||
| // open angle bracket | |||||
| if (token == '{') { | |||||
| getToken(); | |||||
| if (!subgraph) { | |||||
| subgraph = {}; | |||||
| } | |||||
| subgraph.parent = graph; | |||||
| subgraph.node = graph.node; | |||||
| subgraph.edge = graph.edge; | |||||
| subgraph.graph = graph.graph; | |||||
| // statements | |||||
| parseStatements(subgraph); | |||||
| // close angle bracket | |||||
| if (token != '}') { | |||||
| throw newSyntaxError('Angle bracket } expected'); | |||||
| } | |||||
| getToken(); | |||||
| // remove temporary default properties | |||||
| delete subgraph.node; | |||||
| delete subgraph.edge; | |||||
| delete subgraph.graph; | |||||
| delete subgraph.parent; | |||||
| // register at the parent graph | |||||
| if (!graph.subgraphs) { | |||||
| graph.subgraphs = []; | |||||
| } | |||||
| graph.subgraphs.push(subgraph); | |||||
| } | |||||
| return subgraph; | |||||
| } | |||||
| /** | |||||
| * parse an attribute statement like "node [shape=circle fontSize=16]". | |||||
| * Available keywords are 'node', 'edge', 'graph'. | |||||
| * The previous list with default attributes will be replaced | |||||
| * @param {Object} graph | |||||
| * @returns {String | null} keyword Returns the name of the parsed attribute | |||||
| * (node, edge, graph), or null if nothing | |||||
| * is parsed. | |||||
| */ | |||||
| function parseAttributeStatement (graph) { | |||||
| // attribute statements | |||||
| if (token == 'node') { | |||||
| getToken(); | |||||
| // node attributes | |||||
| graph.node = parseAttributeList(); | |||||
| return 'node'; | |||||
| } | |||||
| else if (token == 'edge') { | |||||
| getToken(); | |||||
| // edge attributes | |||||
| graph.edge = parseAttributeList(); | |||||
| return 'edge'; | |||||
| } | |||||
| else if (token == 'graph') { | |||||
| getToken(); | |||||
| // graph attributes | |||||
| graph.graph = parseAttributeList(); | |||||
| return 'graph'; | |||||
| } | |||||
| return null; | |||||
| } | |||||
| /** | |||||
| * parse a node statement | |||||
| * @param {Object} graph | |||||
| * @param {String | Number} id | |||||
| */ | |||||
| function parseNodeStatement(graph, id) { | |||||
| // node statement | |||||
| var node = { | |||||
| id: id | |||||
| }; | |||||
| var attr = parseAttributeList(); | |||||
| if (attr) { | |||||
| node.attr = attr; | |||||
| } | |||||
| addNode(graph, node); | |||||
| // edge statements | |||||
| parseEdge(graph, id); | |||||
| } | |||||
| /** | |||||
| * Parse an edge or a series of edges | |||||
| * @param {Object} graph | |||||
| * @param {String | Number} from Id of the from node | |||||
| */ | |||||
| function parseEdge(graph, from) { | |||||
| while (token == '->' || token == '--') { | |||||
| var to; | |||||
| var type = token; | |||||
| getToken(); | |||||
| var subgraph = parseSubgraph(graph); | |||||
| if (subgraph) { | |||||
| to = subgraph; | |||||
| } | |||||
| else { | |||||
| if (tokenType != TOKENTYPE.IDENTIFIER) { | |||||
| throw newSyntaxError('Identifier or subgraph expected'); | |||||
| } | |||||
| to = token; | |||||
| addNode(graph, { | |||||
| id: to | |||||
| }); | |||||
| getToken(); | |||||
| } | |||||
| // parse edge attributes | |||||
| var attr = parseAttributeList(); | |||||
| // create edge | |||||
| var edge = createEdge(graph, from, to, type, attr); | |||||
| addEdge(graph, edge); | |||||
| from = to; | |||||
| } | |||||
| } | |||||
| /** | |||||
| * Parse a set with attributes, | |||||
| * for example [label="1.000", shape=solid] | |||||
| * @return {Object | null} attr | |||||
| */ | |||||
| function parseAttributeList() { | |||||
| var attr = null; | |||||
| while (token == '[') { | |||||
| getToken(); | |||||
| attr = {}; | |||||
| while (token !== '' && token != ']') { | |||||
| if (tokenType != TOKENTYPE.IDENTIFIER) { | |||||
| throw newSyntaxError('Attribute name expected'); | |||||
| } | |||||
| var name = token; | |||||
| getToken(); | |||||
| if (token != '=') { | |||||
| throw newSyntaxError('Equal sign = expected'); | |||||
| } | |||||
| getToken(); | |||||
| if (tokenType != TOKENTYPE.IDENTIFIER) { | |||||
| throw newSyntaxError('Attribute value expected'); | |||||
| } | |||||
| var value = token; | |||||
| setValue(attr, name, value); // name can be a path | |||||
| getToken(); | |||||
| if (token ==',') { | |||||
| getToken(); | |||||
| } | |||||
| } | |||||
| if (token != ']') { | |||||
| throw newSyntaxError('Bracket ] expected'); | |||||
| } | |||||
| getToken(); | |||||
| } | |||||
| return attr; | |||||
| } | |||||
| /** | |||||
| * Create a syntax error with extra information on current token and index. | |||||
| * @param {String} message | |||||
| * @returns {SyntaxError} err | |||||
| */ | |||||
| function newSyntaxError(message) { | |||||
| return new SyntaxError(message + ', got "' + chop(token, 30) + '" (char ' + index + ')'); | |||||
| } | |||||
| /** | |||||
| * Chop off text after a maximum length | |||||
| * @param {String} text | |||||
| * @param {Number} maxLength | |||||
| * @returns {String} | |||||
| */ | |||||
| function chop (text, maxLength) { | |||||
| return (text.length <= maxLength) ? text : (text.substr(0, 27) + '...'); | |||||
| } | |||||
| /** | |||||
| * Execute a function fn for each pair of elements in two arrays | |||||
| * @param {Array | *} array1 | |||||
| * @param {Array | *} array2 | |||||
| * @param {function} fn | |||||
| */ | |||||
| function forEach2(array1, array2, fn) { | |||||
| if (array1 instanceof Array) { | |||||
| array1.forEach(function (elem1) { | |||||
| if (array2 instanceof Array) { | |||||
| array2.forEach(function (elem2) { | |||||
| fn(elem1, elem2); | |||||
| }); | |||||
| } | |||||
| else { | |||||
| fn(elem1, array2); | |||||
| } | |||||
| }); | |||||
| } | |||||
| else { | |||||
| if (array2 instanceof Array) { | |||||
| array2.forEach(function (elem2) { | |||||
| fn(array1, elem2); | |||||
| }); | |||||
| } | |||||
| else { | |||||
| fn(array1, array2); | |||||
| } | |||||
| } | |||||
| } | |||||
| /** | |||||
| * Convert a string containing a graph in DOT language into a map containing | |||||
| * with nodes and edges in the format of graph. | |||||
| * @param {String} data Text containing a graph in DOT-notation | |||||
| * @return {Object} graphData | |||||
| */ | |||||
| function DOTToGraph (data) { | |||||
| // parse the DOT file | |||||
| var dotData = parseDOT(data); | |||||
| var graphData = { | |||||
| nodes: [], | |||||
| edges: [], | |||||
| options: {} | |||||
| }; | |||||
| // copy the nodes | |||||
| if (dotData.nodes) { | |||||
| dotData.nodes.forEach(function (dotNode) { | |||||
| var graphNode = { | |||||
| id: dotNode.id, | |||||
| label: String(dotNode.label || dotNode.id) | |||||
| }; | |||||
| merge(graphNode, dotNode.attr); | |||||
| if (graphNode.image) { | |||||
| graphNode.shape = 'image'; | |||||
| } | |||||
| graphData.nodes.push(graphNode); | |||||
| }); | |||||
| } | |||||
| // copy the edges | |||||
| if (dotData.edges) { | |||||
| /** | |||||
| * Convert an edge in DOT format to an edge with VisGraph format | |||||
| * @param {Object} dotEdge | |||||
| * @returns {Object} graphEdge | |||||
| */ | |||||
| function convertEdge(dotEdge) { | |||||
| var graphEdge = { | |||||
| from: dotEdge.from, | |||||
| to: dotEdge.to | |||||
| }; | |||||
| merge(graphEdge, dotEdge.attr); | |||||
| graphEdge.style = (dotEdge.type == '->') ? 'arrow' : 'line'; | |||||
| return graphEdge; | |||||
| } | |||||
| dotData.edges.forEach(function (dotEdge) { | |||||
| var from, to; | |||||
| if (dotEdge.from instanceof Object) { | |||||
| from = dotEdge.from.nodes; | |||||
| } | |||||
| else { | |||||
| from = { | |||||
| id: dotEdge.from | |||||
| } | |||||
| } | |||||
| if (dotEdge.to instanceof Object) { | |||||
| to = dotEdge.to.nodes; | |||||
| } | |||||
| else { | |||||
| to = { | |||||
| id: dotEdge.to | |||||
| } | |||||
| } | |||||
| if (dotEdge.from instanceof Object && dotEdge.from.edges) { | |||||
| dotEdge.from.edges.forEach(function (subEdge) { | |||||
| var graphEdge = convertEdge(subEdge); | |||||
| graphData.edges.push(graphEdge); | |||||
| }); | |||||
| } | |||||
| forEach2(from, to, function (from, to) { | |||||
| var subEdge = createEdge(graphData, from.id, to.id, dotEdge.type, dotEdge.attr); | |||||
| var graphEdge = convertEdge(subEdge); | |||||
| graphData.edges.push(graphEdge); | |||||
| }); | |||||
| if (dotEdge.to instanceof Object && dotEdge.to.edges) { | |||||
| dotEdge.to.edges.forEach(function (subEdge) { | |||||
| var graphEdge = convertEdge(subEdge); | |||||
| graphData.edges.push(graphEdge); | |||||
| }); | |||||
| } | |||||
| }); | |||||
| } | |||||
| // copy the options | |||||
| if (dotData.attr) { | |||||
| graphData.options = dotData.attr; | |||||
| } | |||||
| return graphData; | |||||
| } | |||||
| // exports | |||||
| exports.parseDOT = parseDOT; | |||||
| exports.DOTToGraph = DOTToGraph; | |||||
| })(typeof util !== 'undefined' ? util : exports); | |||||
| @ -1,311 +0,0 @@ | |||||
| var HierarchicalLayoutMixin = { | |||||
| _resetLevels : function() { | |||||
| for (var nodeId in this.nodes) { | |||||
| if (this.nodes.hasOwnProperty(nodeId)) { | |||||
| var node = this.nodes[nodeId]; | |||||
| if (node.preassignedLevel == false) { | |||||
| node.level = -1; | |||||
| } | |||||
| } | |||||
| } | |||||
| }, | |||||
| /** | |||||
| * This is the main function to layout the nodes in a hierarchical way. | |||||
| * It checks if the node details are supplied correctly | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| _setupHierarchicalLayout : function() { | |||||
| if (this.constants.hierarchicalLayout.enabled == true && this.nodeIndices.length > 0) { | |||||
| if (this.constants.hierarchicalLayout.direction == "RL" || this.constants.hierarchicalLayout.direction == "DU") { | |||||
| this.constants.hierarchicalLayout.levelSeparation *= -1; | |||||
| } | |||||
| else { | |||||
| this.constants.hierarchicalLayout.levelSeparation = Math.abs(this.constants.hierarchicalLayout.levelSeparation); | |||||
| } | |||||
| // get the size of the largest hubs and check if the user has defined a level for a node. | |||||
| var hubsize = 0; | |||||
| var node, nodeId; | |||||
| var definedLevel = false; | |||||
| var undefinedLevel = false; | |||||
| for (nodeId in this.nodes) { | |||||
| if (this.nodes.hasOwnProperty(nodeId)) { | |||||
| node = this.nodes[nodeId]; | |||||
| if (node.level != -1) { | |||||
| definedLevel = true; | |||||
| } | |||||
| else { | |||||
| undefinedLevel = true; | |||||
| } | |||||
| if (hubsize < node.edges.length) { | |||||
| hubsize = node.edges.length; | |||||
| } | |||||
| } | |||||
| } | |||||
| // if the user defined some levels but not all, alert and run without hierarchical layout | |||||
| if (undefinedLevel == true && definedLevel == true) { | |||||
| alert("To use the hierarchical layout, nodes require either no predefined levels or levels have to be defined for all nodes."); | |||||
| this.zoomExtent(true,this.constants.clustering.enabled); | |||||
| if (!this.constants.clustering.enabled) { | |||||
| this.start(); | |||||
| } | |||||
| } | |||||
| else { | |||||
| // setup the system to use hierarchical method. | |||||
| this._changeConstants(); | |||||
| // define levels if undefined by the users. Based on hubsize | |||||
| if (undefinedLevel == true) { | |||||
| this._determineLevels(hubsize); | |||||
| } | |||||
| // check the distribution of the nodes per level. | |||||
| var distribution = this._getDistribution(); | |||||
| // place the nodes on the canvas. This also stablilizes the system. | |||||
| this._placeNodesByHierarchy(distribution); | |||||
| // start the simulation. | |||||
| this.start(); | |||||
| } | |||||
| } | |||||
| }, | |||||
| /** | |||||
| * This function places the nodes on the canvas based on the hierarchial distribution. | |||||
| * | |||||
| * @param {Object} distribution | obtained by the function this._getDistribution() | |||||
| * @private | |||||
| */ | |||||
| _placeNodesByHierarchy : function(distribution) { | |||||
| var nodeId, node; | |||||
| // start placing all the level 0 nodes first. Then recursively position their branches. | |||||
| for (nodeId in distribution[0].nodes) { | |||||
| if (distribution[0].nodes.hasOwnProperty(nodeId)) { | |||||
| node = distribution[0].nodes[nodeId]; | |||||
| if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") { | |||||
| if (node.xFixed) { | |||||
| node.x = distribution[0].minPos; | |||||
| node.xFixed = false; | |||||
| distribution[0].minPos += distribution[0].nodeSpacing; | |||||
| } | |||||
| } | |||||
| else { | |||||
| if (node.yFixed) { | |||||
| node.y = distribution[0].minPos; | |||||
| node.yFixed = false; | |||||
| distribution[0].minPos += distribution[0].nodeSpacing; | |||||
| } | |||||
| } | |||||
| this._placeBranchNodes(node.edges,node.id,distribution,node.level); | |||||
| } | |||||
| } | |||||
| // stabilize the system after positioning. This function calls zoomExtent. | |||||
| this._stabilize(); | |||||
| }, | |||||
| /** | |||||
| * This function get the distribution of levels based on hubsize | |||||
| * | |||||
| * @returns {Object} | |||||
| * @private | |||||
| */ | |||||
| _getDistribution : function() { | |||||
| var distribution = {}; | |||||
| var nodeId, node, level; | |||||
| // we fix Y because the hierarchy is vertical, we fix X so we do not give a node an x position for a second time. | |||||
| // the fix of X is removed after the x value has been set. | |||||
| for (nodeId in this.nodes) { | |||||
| if (this.nodes.hasOwnProperty(nodeId)) { | |||||
| node = this.nodes[nodeId]; | |||||
| node.xFixed = true; | |||||
| node.yFixed = true; | |||||
| if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") { | |||||
| node.y = this.constants.hierarchicalLayout.levelSeparation*node.level; | |||||
| } | |||||
| else { | |||||
| node.x = this.constants.hierarchicalLayout.levelSeparation*node.level; | |||||
| } | |||||
| if (!distribution.hasOwnProperty(node.level)) { | |||||
| distribution[node.level] = {amount: 0, nodes: {}, minPos:0, nodeSpacing:0}; | |||||
| } | |||||
| distribution[node.level].amount += 1; | |||||
| distribution[node.level].nodes[node.id] = node; | |||||
| } | |||||
| } | |||||
| // determine the largest amount of nodes of all levels | |||||
| var maxCount = 0; | |||||
| for (level in distribution) { | |||||
| if (distribution.hasOwnProperty(level)) { | |||||
| if (maxCount < distribution[level].amount) { | |||||
| maxCount = distribution[level].amount; | |||||
| } | |||||
| } | |||||
| } | |||||
| // set the initial position and spacing of each nodes accordingly | |||||
| for (level in distribution) { | |||||
| if (distribution.hasOwnProperty(level)) { | |||||
| distribution[level].nodeSpacing = (maxCount + 1) * this.constants.hierarchicalLayout.nodeSpacing; | |||||
| distribution[level].nodeSpacing /= (distribution[level].amount + 1); | |||||
| distribution[level].minPos = distribution[level].nodeSpacing - (0.5 * (distribution[level].amount + 1) * distribution[level].nodeSpacing); | |||||
| } | |||||
| } | |||||
| return distribution; | |||||
| }, | |||||
| /** | |||||
| * this function allocates nodes in levels based on the recursive branching from the largest hubs. | |||||
| * | |||||
| * @param hubsize | |||||
| * @private | |||||
| */ | |||||
| _determineLevels : function(hubsize) { | |||||
| var nodeId, node; | |||||
| // determine hubs | |||||
| for (nodeId in this.nodes) { | |||||
| if (this.nodes.hasOwnProperty(nodeId)) { | |||||
| node = this.nodes[nodeId]; | |||||
| if (node.edges.length == hubsize) { | |||||
| node.level = 0; | |||||
| } | |||||
| } | |||||
| } | |||||
| // branch from hubs | |||||
| for (nodeId in this.nodes) { | |||||
| if (this.nodes.hasOwnProperty(nodeId)) { | |||||
| node = this.nodes[nodeId]; | |||||
| if (node.level == 0) { | |||||
| this._setLevel(1,node.edges,node.id); | |||||
| } | |||||
| } | |||||
| } | |||||
| }, | |||||
| /** | |||||
| * Since hierarchical layout does not support: | |||||
| * - smooth curves (based on the physics), | |||||
| * - clustering (based on dynamic node counts) | |||||
| * | |||||
| * We disable both features so there will be no problems. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| _changeConstants : function() { | |||||
| this.constants.clustering.enabled = false; | |||||
| this.constants.physics.barnesHut.enabled = false; | |||||
| this.constants.physics.hierarchicalRepulsion.enabled = true; | |||||
| this._loadSelectedForceSolver(); | |||||
| this.constants.smoothCurves = false; | |||||
| this._configureSmoothCurves(); | |||||
| }, | |||||
| /** | |||||
| * This is a recursively called function to enumerate the branches from the largest hubs and place the nodes | |||||
| * on a X position that ensures there will be no overlap. | |||||
| * | |||||
| * @param edges | |||||
| * @param parentId | |||||
| * @param distribution | |||||
| * @param parentLevel | |||||
| * @private | |||||
| */ | |||||
| _placeBranchNodes : function(edges, parentId, distribution, parentLevel) { | |||||
| for (var i = 0; i < edges.length; i++) { | |||||
| var childNode = null; | |||||
| if (edges[i].toId == parentId) { | |||||
| childNode = edges[i].from; | |||||
| } | |||||
| else { | |||||
| childNode = edges[i].to; | |||||
| } | |||||
| // if a node is conneceted to another node on the same level (or higher (means lower level))!, this is not handled here. | |||||
| var nodeMoved = false; | |||||
| if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") { | |||||
| if (childNode.xFixed && childNode.level > parentLevel) { | |||||
| childNode.xFixed = false; | |||||
| childNode.x = distribution[childNode.level].minPos; | |||||
| nodeMoved = true; | |||||
| } | |||||
| } | |||||
| else { | |||||
| if (childNode.yFixed && childNode.level > parentLevel) { | |||||
| childNode.yFixed = false; | |||||
| childNode.y = distribution[childNode.level].minPos; | |||||
| nodeMoved = true; | |||||
| } | |||||
| } | |||||
| if (nodeMoved == true) { | |||||
| distribution[childNode.level].minPos += distribution[childNode.level].nodeSpacing; | |||||
| if (childNode.edges.length > 1) { | |||||
| this._placeBranchNodes(childNode.edges,childNode.id,distribution,childNode.level); | |||||
| } | |||||
| } | |||||
| } | |||||
| }, | |||||
| /** | |||||
| * this function is called recursively to enumerate the barnches of the largest hubs and give each node a level. | |||||
| * | |||||
| * @param level | |||||
| * @param edges | |||||
| * @param parentId | |||||
| * @private | |||||
| */ | |||||
| _setLevel : function(level, edges, parentId) { | |||||
| for (var i = 0; i < edges.length; i++) { | |||||
| var childNode = null; | |||||
| if (edges[i].toId == parentId) { | |||||
| childNode = edges[i].from; | |||||
| } | |||||
| else { | |||||
| childNode = edges[i].to; | |||||
| } | |||||
| if (childNode.level == -1 || childNode.level > level) { | |||||
| childNode.level = level; | |||||
| if (edges.length > 1) { | |||||
| this._setLevel(level+1, childNode.edges, childNode.id); | |||||
| } | |||||
| } | |||||
| } | |||||
| }, | |||||
| /** | |||||
| * Unfix nodes | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| _restoreNodes : function() { | |||||
| for (nodeId in this.nodes) { | |||||
| if (this.nodes.hasOwnProperty(nodeId)) { | |||||
| this.nodes[nodeId].xFixed = false; | |||||
| this.nodes[nodeId].yFixed = false; | |||||
| } | |||||
| } | |||||
| } | |||||
| }; | |||||
| @ -1,576 +0,0 @@ | |||||
| /** | |||||
| * Created by Alex on 2/4/14. | |||||
| */ | |||||
| var manipulationMixin = { | |||||
| /** | |||||
| * clears the toolbar div element of children | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| _clearManipulatorBar : function() { | |||||
| while (this.manipulationDiv.hasChildNodes()) { | |||||
| this.manipulationDiv.removeChild(this.manipulationDiv.firstChild); | |||||
| } | |||||
| }, | |||||
| /** | |||||
| * Manipulation UI temporarily overloads certain functions to extend or replace them. To be able to restore | |||||
| * these functions to their original functionality, we saved them in this.cachedFunctions. | |||||
| * This function restores these functions to their original function. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| _restoreOverloadedFunctions : function() { | |||||
| for (var functionName in this.cachedFunctions) { | |||||
| if (this.cachedFunctions.hasOwnProperty(functionName)) { | |||||
| this[functionName] = this.cachedFunctions[functionName]; | |||||
| } | |||||
| } | |||||
| }, | |||||
| /** | |||||
| * Enable or disable edit-mode. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| _toggleEditMode : function() { | |||||
| this.editMode = !this.editMode; | |||||
| var toolbar = document.getElementById("network-manipulationDiv"); | |||||
| var closeDiv = document.getElementById("network-manipulation-closeDiv"); | |||||
| var editModeDiv = document.getElementById("network-manipulation-editMode"); | |||||
| if (this.editMode == true) { | |||||
| toolbar.style.display="block"; | |||||
| closeDiv.style.display="block"; | |||||
| editModeDiv.style.display="none"; | |||||
| closeDiv.onclick = this._toggleEditMode.bind(this); | |||||
| } | |||||
| else { | |||||
| toolbar.style.display="none"; | |||||
| closeDiv.style.display="none"; | |||||
| editModeDiv.style.display="block"; | |||||
| closeDiv.onclick = null; | |||||
| } | |||||
| this._createManipulatorBar() | |||||
| }, | |||||
| /** | |||||
| * main function, creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| _createManipulatorBar : function() { | |||||
| // remove bound functions | |||||
| if (this.boundFunction) { | |||||
| this.off('select', this.boundFunction); | |||||
| } | |||||
| if (this.edgeBeingEdited !== undefined) { | |||||
| this.edgeBeingEdited._disableControlNodes(); | |||||
| this.edgeBeingEdited = undefined; | |||||
| this.selectedControlNode = null; | |||||
| } | |||||
| // restore overloaded functions | |||||
| this._restoreOverloadedFunctions(); | |||||
| // resume calculation | |||||
| this.freezeSimulation = false; | |||||
| // reset global variables | |||||
| this.blockConnectingEdgeSelection = false; | |||||
| this.forceAppendSelection = false; | |||||
| if (this.editMode == true) { | |||||
| while (this.manipulationDiv.hasChildNodes()) { | |||||
| this.manipulationDiv.removeChild(this.manipulationDiv.firstChild); | |||||
| } | |||||
| // add the icons to the manipulator div | |||||
| this.manipulationDiv.innerHTML = "" + | |||||
| "<span class='network-manipulationUI add' id='network-manipulate-addNode'>" + | |||||
| "<span class='network-manipulationLabel'>"+this.constants.labels['add'] +"</span></span>" + | |||||
| "<div class='network-seperatorLine'></div>" + | |||||
| "<span class='network-manipulationUI connect' id='network-manipulate-connectNode'>" + | |||||
| "<span class='network-manipulationLabel'>"+this.constants.labels['link'] +"</span></span>"; | |||||
| if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) { | |||||
| this.manipulationDiv.innerHTML += "" + | |||||
| "<div class='network-seperatorLine'></div>" + | |||||
| "<span class='network-manipulationUI edit' id='network-manipulate-editNode'>" + | |||||
| "<span class='network-manipulationLabel'>"+this.constants.labels['editNode'] +"</span></span>"; | |||||
| } | |||||
| else if (this._getSelectedEdgeCount() == 1 && this._getSelectedNodeCount() == 0) { | |||||
| this.manipulationDiv.innerHTML += "" + | |||||
| "<div class='network-seperatorLine'></div>" + | |||||
| "<span class='network-manipulationUI edit' id='network-manipulate-editEdge'>" + | |||||
| "<span class='network-manipulationLabel'>"+this.constants.labels['editEdge'] +"</span></span>"; | |||||
| } | |||||
| if (this._selectionIsEmpty() == false) { | |||||
| this.manipulationDiv.innerHTML += "" + | |||||
| "<div class='network-seperatorLine'></div>" + | |||||
| "<span class='network-manipulationUI delete' id='network-manipulate-delete'>" + | |||||
| "<span class='network-manipulationLabel'>"+this.constants.labels['del'] +"</span></span>"; | |||||
| } | |||||
| // bind the icons | |||||
| var addNodeButton = document.getElementById("network-manipulate-addNode"); | |||||
| addNodeButton.onclick = this._createAddNodeToolbar.bind(this); | |||||
| var addEdgeButton = document.getElementById("network-manipulate-connectNode"); | |||||
| addEdgeButton.onclick = this._createAddEdgeToolbar.bind(this); | |||||
| if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) { | |||||
| var editButton = document.getElementById("network-manipulate-editNode"); | |||||
| editButton.onclick = this._editNode.bind(this); | |||||
| } | |||||
| else if (this._getSelectedEdgeCount() == 1 && this._getSelectedNodeCount() == 0) { | |||||
| var editButton = document.getElementById("network-manipulate-editEdge"); | |||||
| editButton.onclick = this._createEditEdgeToolbar.bind(this); | |||||
| } | |||||
| if (this._selectionIsEmpty() == false) { | |||||
| var deleteButton = document.getElementById("network-manipulate-delete"); | |||||
| deleteButton.onclick = this._deleteSelected.bind(this); | |||||
| } | |||||
| var closeDiv = document.getElementById("network-manipulation-closeDiv"); | |||||
| closeDiv.onclick = this._toggleEditMode.bind(this); | |||||
| this.boundFunction = this._createManipulatorBar.bind(this); | |||||
| this.on('select', this.boundFunction); | |||||
| } | |||||
| else { | |||||
| this.editModeDiv.innerHTML = "" + | |||||
| "<span class='network-manipulationUI edit editmode' id='network-manipulate-editModeButton'>" + | |||||
| "<span class='network-manipulationLabel'>" + this.constants.labels['edit'] + "</span></span>"; | |||||
| var editModeButton = document.getElementById("network-manipulate-editModeButton"); | |||||
| editModeButton.onclick = this._toggleEditMode.bind(this); | |||||
| } | |||||
| }, | |||||
| /** | |||||
| * Create the toolbar for adding Nodes | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| _createAddNodeToolbar : function() { | |||||
| // clear the toolbar | |||||
| this._clearManipulatorBar(); | |||||
| if (this.boundFunction) { | |||||
| this.off('select', this.boundFunction); | |||||
| } | |||||
| // create the toolbar contents | |||||
| this.manipulationDiv.innerHTML = "" + | |||||
| "<span class='network-manipulationUI back' id='network-manipulate-back'>" + | |||||
| "<span class='network-manipulationLabel'>" + this.constants.labels['back'] + " </span></span>" + | |||||
| "<div class='network-seperatorLine'></div>" + | |||||
| "<span class='network-manipulationUI none' id='network-manipulate-back'>" + | |||||
| "<span id='network-manipulatorLabel' class='network-manipulationLabel'>" + this.constants.labels['addDescription'] + "</span></span>"; | |||||
| // bind the icon | |||||
| var backButton = document.getElementById("network-manipulate-back"); | |||||
| backButton.onclick = this._createManipulatorBar.bind(this); | |||||
| // we use the boundFunction so we can reference it when we unbind it from the "select" event. | |||||
| this.boundFunction = this._addNode.bind(this); | |||||
| this.on('select', this.boundFunction); | |||||
| }, | |||||
| /** | |||||
| * create the toolbar to connect nodes | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| _createAddEdgeToolbar : function() { | |||||
| // clear the toolbar | |||||
| this._clearManipulatorBar(); | |||||
| this._unselectAll(true); | |||||
| this.freezeSimulation = true; | |||||
| if (this.boundFunction) { | |||||
| this.off('select', this.boundFunction); | |||||
| } | |||||
| this._unselectAll(); | |||||
| this.forceAppendSelection = false; | |||||
| this.blockConnectingEdgeSelection = true; | |||||
| this.manipulationDiv.innerHTML = "" + | |||||
| "<span class='network-manipulationUI back' id='network-manipulate-back'>" + | |||||
| "<span class='network-manipulationLabel'>" + this.constants.labels['back'] + " </span></span>" + | |||||
| "<div class='network-seperatorLine'></div>" + | |||||
| "<span class='network-manipulationUI none' id='network-manipulate-back'>" + | |||||
| "<span id='network-manipulatorLabel' class='network-manipulationLabel'>" + this.constants.labels['linkDescription'] + "</span></span>"; | |||||
| // bind the icon | |||||
| var backButton = document.getElementById("network-manipulate-back"); | |||||
| backButton.onclick = this._createManipulatorBar.bind(this); | |||||
| // we use the boundFunction so we can reference it when we unbind it from the "select" event. | |||||
| this.boundFunction = this._handleConnect.bind(this); | |||||
| this.on('select', this.boundFunction); | |||||
| // temporarily overload functions | |||||
| this.cachedFunctions["_handleTouch"] = this._handleTouch; | |||||
| this.cachedFunctions["_handleOnRelease"] = this._handleOnRelease; | |||||
| this._handleTouch = this._handleConnect; | |||||
| this._handleOnRelease = this._finishConnect; | |||||
| // redraw to show the unselect | |||||
| this._redraw(); | |||||
| }, | |||||
| /** | |||||
| * create the toolbar to edit edges | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| _createEditEdgeToolbar : function() { | |||||
| // clear the toolbar | |||||
| this._clearManipulatorBar(); | |||||
| if (this.boundFunction) { | |||||
| this.off('select', this.boundFunction); | |||||
| } | |||||
| this.edgeBeingEdited = this._getSelectedEdge(); | |||||
| this.edgeBeingEdited._enableControlNodes(); | |||||
| this.manipulationDiv.innerHTML = "" + | |||||
| "<span class='network-manipulationUI back' id='network-manipulate-back'>" + | |||||
| "<span class='network-manipulationLabel'>" + this.constants.labels['back'] + " </span></span>" + | |||||
| "<div class='network-seperatorLine'></div>" + | |||||
| "<span class='network-manipulationUI none' id='network-manipulate-back'>" + | |||||
| "<span id='network-manipulatorLabel' class='network-manipulationLabel'>" + this.constants.labels['editEdgeDescription'] + "</span></span>"; | |||||
| // bind the icon | |||||
| var backButton = document.getElementById("network-manipulate-back"); | |||||
| backButton.onclick = this._createManipulatorBar.bind(this); | |||||
| // temporarily overload functions | |||||
| this.cachedFunctions["_handleTouch"] = this._handleTouch; | |||||
| this.cachedFunctions["_handleOnRelease"] = this._handleOnRelease; | |||||
| this.cachedFunctions["_handleTap"] = this._handleTap; | |||||
| this.cachedFunctions["_handleDragStart"] = this._handleDragStart; | |||||
| this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag; | |||||
| this._handleTouch = this._selectControlNode; | |||||
| this._handleTap = function () {}; | |||||
| this._handleOnDrag = this._controlNodeDrag; | |||||
| this._handleDragStart = function () {} | |||||
| this._handleOnRelease = this._releaseControlNode; | |||||
| // redraw to show the unselect | |||||
| this._redraw(); | |||||
| }, | |||||
| /** | |||||
| * the function bound to the selection event. It checks if you want to connect a cluster and changes the description | |||||
| * to walk the user through the process. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| _selectControlNode : function(pointer) { | |||||
| this.edgeBeingEdited.controlNodes.from.unselect(); | |||||
| this.edgeBeingEdited.controlNodes.to.unselect(); | |||||
| this.selectedControlNode = this.edgeBeingEdited._getSelectedControlNode(this._XconvertDOMtoCanvas(pointer.x),this._YconvertDOMtoCanvas(pointer.y)); | |||||
| if (this.selectedControlNode !== null) { | |||||
| this.selectedControlNode.select(); | |||||
| this.freezeSimulation = true; | |||||
| } | |||||
| this._redraw(); | |||||
| }, | |||||
| /** | |||||
| * the function bound to the selection event. It checks if you want to connect a cluster and changes the description | |||||
| * to walk the user through the process. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| _controlNodeDrag : function(event) { | |||||
| var pointer = this._getPointer(event.gesture.center); | |||||
| if (this.selectedControlNode !== null && this.selectedControlNode !== undefined) { | |||||
| this.selectedControlNode.x = this._XconvertDOMtoCanvas(pointer.x); | |||||
| this.selectedControlNode.y = this._YconvertDOMtoCanvas(pointer.y); | |||||
| } | |||||
| this._redraw(); | |||||
| }, | |||||
| _releaseControlNode : function(pointer) { | |||||
| var newNode = this._getNodeAt(pointer); | |||||
| if (newNode != null) { | |||||
| if (this.edgeBeingEdited.controlNodes.from.selected == true) { | |||||
| this._editEdge(newNode.id, this.edgeBeingEdited.to.id); | |||||
| this.edgeBeingEdited.controlNodes.from.unselect(); | |||||
| } | |||||
| if (this.edgeBeingEdited.controlNodes.to.selected == true) { | |||||
| this._editEdge(this.edgeBeingEdited.from.id, newNode.id); | |||||
| this.edgeBeingEdited.controlNodes.to.unselect(); | |||||
| } | |||||
| } | |||||
| else { | |||||
| this.edgeBeingEdited._restoreControlNodes(); | |||||
| } | |||||
| this.freezeSimulation = false; | |||||
| this._redraw(); | |||||
| }, | |||||
| /** | |||||
| * the function bound to the selection event. It checks if you want to connect a cluster and changes the description | |||||
| * to walk the user through the process. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| _handleConnect : function(pointer) { | |||||
| if (this._getSelectedNodeCount() == 0) { | |||||
| var node = this._getNodeAt(pointer); | |||||
| if (node != null) { | |||||
| if (node.clusterSize > 1) { | |||||
| alert("Cannot create edges to a cluster.") | |||||
| } | |||||
| else { | |||||
| this._selectObject(node,false); | |||||
| // create a node the temporary line can look at | |||||
| this.sectors['support']['nodes']['targetNode'] = new Node({id:'targetNode'},{},{},this.constants); | |||||
| this.sectors['support']['nodes']['targetNode'].x = node.x; | |||||
| this.sectors['support']['nodes']['targetNode'].y = node.y; | |||||
| this.sectors['support']['nodes']['targetViaNode'] = new Node({id:'targetViaNode'},{},{},this.constants); | |||||
| this.sectors['support']['nodes']['targetViaNode'].x = node.x; | |||||
| this.sectors['support']['nodes']['targetViaNode'].y = node.y; | |||||
| this.sectors['support']['nodes']['targetViaNode'].parentEdgeId = "connectionEdge"; | |||||
| // create a temporary edge | |||||
| this.edges['connectionEdge'] = new Edge({id:"connectionEdge",from:node.id,to:this.sectors['support']['nodes']['targetNode'].id}, this, this.constants); | |||||
| this.edges['connectionEdge'].from = node; | |||||
| this.edges['connectionEdge'].connected = true; | |||||
| this.edges['connectionEdge'].smooth = true; | |||||
| this.edges['connectionEdge'].selected = true; | |||||
| this.edges['connectionEdge'].to = this.sectors['support']['nodes']['targetNode']; | |||||
| this.edges['connectionEdge'].via = this.sectors['support']['nodes']['targetViaNode']; | |||||
| this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag; | |||||
| this._handleOnDrag = function(event) { | |||||
| var pointer = this._getPointer(event.gesture.center); | |||||
| this.sectors['support']['nodes']['targetNode'].x = this._XconvertDOMtoCanvas(pointer.x); | |||||
| this.sectors['support']['nodes']['targetNode'].y = this._YconvertDOMtoCanvas(pointer.y); | |||||
| this.sectors['support']['nodes']['targetViaNode'].x = 0.5 * (this._XconvertDOMtoCanvas(pointer.x) + this.edges['connectionEdge'].from.x); | |||||
| this.sectors['support']['nodes']['targetViaNode'].y = this._YconvertDOMtoCanvas(pointer.y); | |||||
| }; | |||||
| this.moving = true; | |||||
| this.start(); | |||||
| } | |||||
| } | |||||
| } | |||||
| }, | |||||
| _finishConnect : function(pointer) { | |||||
| if (this._getSelectedNodeCount() == 1) { | |||||
| // restore the drag function | |||||
| this._handleOnDrag = this.cachedFunctions["_handleOnDrag"]; | |||||
| delete this.cachedFunctions["_handleOnDrag"]; | |||||
| // remember the edge id | |||||
| var connectFromId = this.edges['connectionEdge'].fromId; | |||||
| // remove the temporary nodes and edge | |||||
| delete this.edges['connectionEdge']; | |||||
| delete this.sectors['support']['nodes']['targetNode']; | |||||
| delete this.sectors['support']['nodes']['targetViaNode']; | |||||
| var node = this._getNodeAt(pointer); | |||||
| if (node != null) { | |||||
| if (node.clusterSize > 1) { | |||||
| alert("Cannot create edges to a cluster.") | |||||
| } | |||||
| else { | |||||
| this._createEdge(connectFromId,node.id); | |||||
| this._createManipulatorBar(); | |||||
| } | |||||
| } | |||||
| this._unselectAll(); | |||||
| } | |||||
| }, | |||||
| /** | |||||
| * Adds a node on the specified location | |||||
| */ | |||||
| _addNode : function() { | |||||
| if (this._selectionIsEmpty() && this.editMode == true) { | |||||
| var positionObject = this._pointerToPositionObject(this.pointerPosition); | |||||
| var defaultData = {id:util.randomUUID(),x:positionObject.left,y:positionObject.top,label:"new",allowedToMoveX:true,allowedToMoveY:true}; | |||||
| if (this.triggerFunctions.add) { | |||||
| if (this.triggerFunctions.add.length == 2) { | |||||
| var me = this; | |||||
| this.triggerFunctions.add(defaultData, function(finalizedData) { | |||||
| me.nodesData.add(finalizedData); | |||||
| me._createManipulatorBar(); | |||||
| me.moving = true; | |||||
| me.start(); | |||||
| }); | |||||
| } | |||||
| else { | |||||
| alert(this.constants.labels['addError']); | |||||
| this._createManipulatorBar(); | |||||
| this.moving = true; | |||||
| this.start(); | |||||
| } | |||||
| } | |||||
| else { | |||||
| this.nodesData.add(defaultData); | |||||
| this._createManipulatorBar(); | |||||
| this.moving = true; | |||||
| this.start(); | |||||
| } | |||||
| } | |||||
| }, | |||||
| /** | |||||
| * connect two nodes with a new edge. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| _createEdge : function(sourceNodeId,targetNodeId) { | |||||
| if (this.editMode == true) { | |||||
| var defaultData = {from:sourceNodeId, to:targetNodeId}; | |||||
| if (this.triggerFunctions.connect) { | |||||
| if (this.triggerFunctions.connect.length == 2) { | |||||
| var me = this; | |||||
| this.triggerFunctions.connect(defaultData, function(finalizedData) { | |||||
| me.edgesData.add(finalizedData); | |||||
| me.moving = true; | |||||
| me.start(); | |||||
| }); | |||||
| } | |||||
| else { | |||||
| alert(this.constants.labels["linkError"]); | |||||
| this.moving = true; | |||||
| this.start(); | |||||
| } | |||||
| } | |||||
| else { | |||||
| this.edgesData.add(defaultData); | |||||
| this.moving = true; | |||||
| this.start(); | |||||
| } | |||||
| } | |||||
| }, | |||||
| /** | |||||
| * connect two nodes with a new edge. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| _editEdge : function(sourceNodeId,targetNodeId) { | |||||
| if (this.editMode == true) { | |||||
| var defaultData = {id: this.edgeBeingEdited.id, from:sourceNodeId, to:targetNodeId}; | |||||
| if (this.triggerFunctions.editEdge) { | |||||
| if (this.triggerFunctions.editEdge.length == 2) { | |||||
| var me = this; | |||||
| this.triggerFunctions.editEdge(defaultData, function(finalizedData) { | |||||
| me.edgesData.update(finalizedData); | |||||
| me.moving = true; | |||||
| me.start(); | |||||
| }); | |||||
| } | |||||
| else { | |||||
| alert(this.constants.labels["linkError"]); | |||||
| this.moving = true; | |||||
| this.start(); | |||||
| } | |||||
| } | |||||
| else { | |||||
| this.edgesData.update(defaultData); | |||||
| this.moving = true; | |||||
| this.start(); | |||||
| } | |||||
| } | |||||
| }, | |||||
| /** | |||||
| * Create the toolbar to edit the selected node. The label and the color can be changed. Other colors are derived from the chosen color. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| _editNode : function() { | |||||
| if (this.triggerFunctions.edit && this.editMode == true) { | |||||
| var node = this._getSelectedNode(); | |||||
| var data = {id:node.id, | |||||
| label: node.label, | |||||
| group: node.group, | |||||
| shape: node.shape, | |||||
| color: { | |||||
| background:node.color.background, | |||||
| border:node.color.border, | |||||
| highlight: { | |||||
| background:node.color.highlight.background, | |||||
| border:node.color.highlight.border | |||||
| } | |||||
| }}; | |||||
| if (this.triggerFunctions.edit.length == 2) { | |||||
| var me = this; | |||||
| this.triggerFunctions.edit(data, function (finalizedData) { | |||||
| me.nodesData.update(finalizedData); | |||||
| me._createManipulatorBar(); | |||||
| me.moving = true; | |||||
| me.start(); | |||||
| }); | |||||
| } | |||||
| else { | |||||
| alert(this.constants.labels["editError"]); | |||||
| } | |||||
| } | |||||
| else { | |||||
| alert(this.constants.labels["editBoundError"]); | |||||
| } | |||||
| }, | |||||
| /** | |||||
| * delete everything in the selection | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| _deleteSelected : function() { | |||||
| if (!this._selectionIsEmpty() && this.editMode == true) { | |||||
| if (!this._clusterInSelection()) { | |||||
| var selectedNodes = this.getSelectedNodes(); | |||||
| var selectedEdges = this.getSelectedEdges(); | |||||
| if (this.triggerFunctions.del) { | |||||
| var me = this; | |||||
| var data = {nodes: selectedNodes, edges: selectedEdges}; | |||||
| if (this.triggerFunctions.del.length = 2) { | |||||
| this.triggerFunctions.del(data, function (finalizedData) { | |||||
| me.edgesData.remove(finalizedData.edges); | |||||
| me.nodesData.remove(finalizedData.nodes); | |||||
| me._unselectAll(); | |||||
| me.moving = true; | |||||
| me.start(); | |||||
| }); | |||||
| } | |||||
| else { | |||||
| alert(this.constants.labels["deleteError"]) | |||||
| } | |||||
| } | |||||
| else { | |||||
| this.edgesData.remove(selectedEdges); | |||||
| this.nodesData.remove(selectedNodes); | |||||
| this._unselectAll(); | |||||
| this.moving = true; | |||||
| this.start(); | |||||
| } | |||||
| } | |||||
| else { | |||||
| alert(this.constants.labels["deleteClusterError"]); | |||||
| } | |||||
| } | |||||
| } | |||||
| }; | |||||
| @ -1,199 +0,0 @@ | |||||
| /** | |||||
| * Created by Alex on 2/10/14. | |||||
| */ | |||||
| var networkMixinLoaders = { | |||||
| /** | |||||
| * Load a mixin into the network object | |||||
| * | |||||
| * @param {Object} sourceVariable | this object has to contain functions. | |||||
| * @private | |||||
| */ | |||||
| _loadMixin: function (sourceVariable) { | |||||
| for (var mixinFunction in sourceVariable) { | |||||
| if (sourceVariable.hasOwnProperty(mixinFunction)) { | |||||
| Network.prototype[mixinFunction] = sourceVariable[mixinFunction]; | |||||
| } | |||||
| } | |||||
| }, | |||||
| /** | |||||
| * removes a mixin from the network object. | |||||
| * | |||||
| * @param {Object} sourceVariable | this object has to contain functions. | |||||
| * @private | |||||
| */ | |||||
| _clearMixin: function (sourceVariable) { | |||||
| for (var mixinFunction in sourceVariable) { | |||||
| if (sourceVariable.hasOwnProperty(mixinFunction)) { | |||||
| Network.prototype[mixinFunction] = undefined; | |||||
| } | |||||
| } | |||||
| }, | |||||
| /** | |||||
| * Mixin the physics system and initialize the parameters required. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| _loadPhysicsSystem: function () { | |||||
| this._loadMixin(physicsMixin); | |||||
| this._loadSelectedForceSolver(); | |||||
| if (this.constants.configurePhysics == true) { | |||||
| this._loadPhysicsConfiguration(); | |||||
| } | |||||
| }, | |||||
| /** | |||||
| * Mixin the cluster system and initialize the parameters required. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| _loadClusterSystem: function () { | |||||
| this.clusterSession = 0; | |||||
| this.hubThreshold = 5; | |||||
| this._loadMixin(ClusterMixin); | |||||
| }, | |||||
| /** | |||||
| * Mixin the sector system and initialize the parameters required | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| _loadSectorSystem: function () { | |||||
| this.sectors = {}; | |||||
| this.activeSector = ["default"]; | |||||
| this.sectors["active"] = {}; | |||||
| this.sectors["active"]["default"] = {"nodes": {}, | |||||
| "edges": {}, | |||||
| "nodeIndices": [], | |||||
| "formationScale": 1.0, | |||||
| "drawingNode": undefined }; | |||||
| this.sectors["frozen"] = {}; | |||||
| this.sectors["support"] = {"nodes": {}, | |||||
| "edges": {}, | |||||
| "nodeIndices": [], | |||||
| "formationScale": 1.0, | |||||
| "drawingNode": undefined }; | |||||
| this.nodeIndices = this.sectors["active"]["default"]["nodeIndices"]; // the node indices list is used to speed up the computation of the repulsion fields | |||||
| this._loadMixin(SectorMixin); | |||||
| }, | |||||
| /** | |||||
| * Mixin the selection system and initialize the parameters required | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| _loadSelectionSystem: function () { | |||||
| this.selectionObj = {nodes: {}, edges: {}}; | |||||
| this._loadMixin(SelectionMixin); | |||||
| }, | |||||
| /** | |||||
| * Mixin the navigationUI (User Interface) system and initialize the parameters required | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| _loadManipulationSystem: function () { | |||||
| // reset global variables -- these are used by the selection of nodes and edges. | |||||
| this.blockConnectingEdgeSelection = false; | |||||
| this.forceAppendSelection = false; | |||||
| if (this.constants.dataManipulation.enabled == true) { | |||||
| // load the manipulator HTML elements. All styling done in css. | |||||
| if (this.manipulationDiv === undefined) { | |||||
| this.manipulationDiv = document.createElement('div'); | |||||
| this.manipulationDiv.className = 'network-manipulationDiv'; | |||||
| this.manipulationDiv.id = 'network-manipulationDiv'; | |||||
| if (this.editMode == true) { | |||||
| this.manipulationDiv.style.display = "block"; | |||||
| } | |||||
| else { | |||||
| this.manipulationDiv.style.display = "none"; | |||||
| } | |||||
| this.containerElement.insertBefore(this.manipulationDiv, this.frame); | |||||
| } | |||||
| if (this.editModeDiv === undefined) { | |||||
| this.editModeDiv = document.createElement('div'); | |||||
| this.editModeDiv.className = 'network-manipulation-editMode'; | |||||
| this.editModeDiv.id = 'network-manipulation-editMode'; | |||||
| if (this.editMode == true) { | |||||
| this.editModeDiv.style.display = "none"; | |||||
| } | |||||
| else { | |||||
| this.editModeDiv.style.display = "block"; | |||||
| } | |||||
| this.containerElement.insertBefore(this.editModeDiv, this.frame); | |||||
| } | |||||
| if (this.closeDiv === undefined) { | |||||
| this.closeDiv = document.createElement('div'); | |||||
| this.closeDiv.className = 'network-manipulation-closeDiv'; | |||||
| this.closeDiv.id = 'network-manipulation-closeDiv'; | |||||
| this.closeDiv.style.display = this.manipulationDiv.style.display; | |||||
| this.containerElement.insertBefore(this.closeDiv, this.frame); | |||||
| } | |||||
| // load the manipulation functions | |||||
| this._loadMixin(manipulationMixin); | |||||
| // create the manipulator toolbar | |||||
| this._createManipulatorBar(); | |||||
| } | |||||
| else { | |||||
| if (this.manipulationDiv !== undefined) { | |||||
| // removes all the bindings and overloads | |||||
| this._createManipulatorBar(); | |||||
| // remove the manipulation divs | |||||
| this.containerElement.removeChild(this.manipulationDiv); | |||||
| this.containerElement.removeChild(this.editModeDiv); | |||||
| this.containerElement.removeChild(this.closeDiv); | |||||
| this.manipulationDiv = undefined; | |||||
| this.editModeDiv = undefined; | |||||
| this.closeDiv = undefined; | |||||
| // remove the mixin functions | |||||
| this._clearMixin(manipulationMixin); | |||||
| } | |||||
| } | |||||
| }, | |||||
| /** | |||||
| * Mixin the navigation (User Interface) system and initialize the parameters required | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| _loadNavigationControls: function () { | |||||
| this._loadMixin(NavigationMixin); | |||||
| // the clean function removes the button divs, this is done to remove the bindings. | |||||
| this._cleanNavigation(); | |||||
| if (this.constants.navigation.enabled == true) { | |||||
| this._loadNavigationElements(); | |||||
| } | |||||
| }, | |||||
| /** | |||||
| * Mixin the hierarchical layout system. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| _loadHierarchySystem: function () { | |||||
| this._loadMixin(HierarchicalLayoutMixin); | |||||
| } | |||||
| }; | |||||
| @ -1,205 +0,0 @@ | |||||
| /** | |||||
| * Created by Alex on 1/22/14. | |||||
| */ | |||||
| var NavigationMixin = { | |||||
| _cleanNavigation : function() { | |||||
| // clean up previosu navigation items | |||||
| var wrapper = document.getElementById('network-navigation_wrapper'); | |||||
| if (wrapper != null) { | |||||
| this.containerElement.removeChild(wrapper); | |||||
| } | |||||
| document.onmouseup = null; | |||||
| }, | |||||
| /** | |||||
| * Creation of the navigation controls nodes. They are drawn over the rest of the nodes and are not affected by scale and translation | |||||
| * they have a triggerFunction which is called on click. If the position of the navigation controls is dependent | |||||
| * on this.frame.canvas.clientWidth or this.frame.canvas.clientHeight, we flag horizontalAlignLeft and verticalAlignTop false. | |||||
| * This means that the location will be corrected by the _relocateNavigation function on a size change of the canvas. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| _loadNavigationElements : function() { | |||||
| this._cleanNavigation(); | |||||
| this.navigationDivs = {}; | |||||
| var navigationDivs = ['up','down','left','right','zoomIn','zoomOut','zoomExtends']; | |||||
| var navigationDivActions = ['_moveUp','_moveDown','_moveLeft','_moveRight','_zoomIn','_zoomOut','zoomExtent']; | |||||
| this.navigationDivs['wrapper'] = document.createElement('div'); | |||||
| this.navigationDivs['wrapper'].id = "network-navigation_wrapper"; | |||||
| this.navigationDivs['wrapper'].style.position = "absolute"; | |||||
| this.navigationDivs['wrapper'].style.width = this.frame.canvas.clientWidth + "px"; | |||||
| this.navigationDivs['wrapper'].style.height = this.frame.canvas.clientHeight + "px"; | |||||
| this.containerElement.insertBefore(this.navigationDivs['wrapper'],this.frame); | |||||
| for (var i = 0; i < navigationDivs.length; i++) { | |||||
| this.navigationDivs[navigationDivs[i]] = document.createElement('div'); | |||||
| this.navigationDivs[navigationDivs[i]].id = "network-navigation_" + navigationDivs[i]; | |||||
| this.navigationDivs[navigationDivs[i]].className = "network-navigation " + navigationDivs[i]; | |||||
| this.navigationDivs['wrapper'].appendChild(this.navigationDivs[navigationDivs[i]]); | |||||
| this.navigationDivs[navigationDivs[i]].onmousedown = this[navigationDivActions[i]].bind(this); | |||||
| } | |||||
| document.onmouseup = this._stopMovement.bind(this); | |||||
| }, | |||||
| /** | |||||
| * this stops all movement induced by the navigation buttons | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| _stopMovement : function() { | |||||
| this._xStopMoving(); | |||||
| this._yStopMoving(); | |||||
| this._stopZoom(); | |||||
| }, | |||||
| /** | |||||
| * stops the actions performed by page up and down etc. | |||||
| * | |||||
| * @param event | |||||
| * @private | |||||
| */ | |||||
| _preventDefault : function(event) { | |||||
| if (event !== undefined) { | |||||
| if (event.preventDefault) { | |||||
| event.preventDefault(); | |||||
| } else { | |||||
| event.returnValue = false; | |||||
| } | |||||
| } | |||||
| }, | |||||
| /** | |||||
| * move the screen up | |||||
| * By using the increments, instead of adding a fixed number to the translation, we keep fluent and | |||||
| * instant movement. The onKeypress event triggers immediately, then pauses, then triggers frequently | |||||
| * To avoid this behaviour, we do the translation in the start loop. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| _moveUp : function(event) { | |||||
| this.yIncrement = this.constants.keyboard.speed.y; | |||||
| this.start(); // if there is no node movement, the calculation wont be done | |||||
| this._preventDefault(event); | |||||
| if (this.navigationDivs) { | |||||
| this.navigationDivs['up'].className += " active"; | |||||
| } | |||||
| }, | |||||
| /** | |||||
| * move the screen down | |||||
| * @private | |||||
| */ | |||||
| _moveDown : function(event) { | |||||
| this.yIncrement = -this.constants.keyboard.speed.y; | |||||
| this.start(); // if there is no node movement, the calculation wont be done | |||||
| this._preventDefault(event); | |||||
| if (this.navigationDivs) { | |||||
| this.navigationDivs['down'].className += " active"; | |||||
| } | |||||
| }, | |||||
| /** | |||||
| * move the screen left | |||||
| * @private | |||||
| */ | |||||
| _moveLeft : function(event) { | |||||
| this.xIncrement = this.constants.keyboard.speed.x; | |||||
| this.start(); // if there is no node movement, the calculation wont be done | |||||
| this._preventDefault(event); | |||||
| if (this.navigationDivs) { | |||||
| this.navigationDivs['left'].className += " active"; | |||||
| } | |||||
| }, | |||||
| /** | |||||
| * move the screen right | |||||
| * @private | |||||
| */ | |||||
| _moveRight : function(event) { | |||||
| this.xIncrement = -this.constants.keyboard.speed.y; | |||||
| this.start(); // if there is no node movement, the calculation wont be done | |||||
| this._preventDefault(event); | |||||
| if (this.navigationDivs) { | |||||
| this.navigationDivs['right'].className += " active"; | |||||
| } | |||||
| }, | |||||
| /** | |||||
| * Zoom in, using the same method as the movement. | |||||
| * @private | |||||
| */ | |||||
| _zoomIn : function(event) { | |||||
| this.zoomIncrement = this.constants.keyboard.speed.zoom; | |||||
| this.start(); // if there is no node movement, the calculation wont be done | |||||
| this._preventDefault(event); | |||||
| if (this.navigationDivs) { | |||||
| this.navigationDivs['zoomIn'].className += " active"; | |||||
| } | |||||
| }, | |||||
| /** | |||||
| * Zoom out | |||||
| * @private | |||||
| */ | |||||
| _zoomOut : function() { | |||||
| this.zoomIncrement = -this.constants.keyboard.speed.zoom; | |||||
| this.start(); // if there is no node movement, the calculation wont be done | |||||
| this._preventDefault(event); | |||||
| if (this.navigationDivs) { | |||||
| this.navigationDivs['zoomOut'].className += " active"; | |||||
| } | |||||
| }, | |||||
| /** | |||||
| * Stop zooming and unhighlight the zoom controls | |||||
| * @private | |||||
| */ | |||||
| _stopZoom : function() { | |||||
| this.zoomIncrement = 0; | |||||
| if (this.navigationDivs) { | |||||
| this.navigationDivs['zoomIn'].className = this.navigationDivs['zoomIn'].className.replace(" active",""); | |||||
| this.navigationDivs['zoomOut'].className = this.navigationDivs['zoomOut'].className.replace(" active",""); | |||||
| } | |||||
| }, | |||||
| /** | |||||
| * Stop moving in the Y direction and unHighlight the up and down | |||||
| * @private | |||||
| */ | |||||
| _yStopMoving : function() { | |||||
| this.yIncrement = 0; | |||||
| if (this.navigationDivs) { | |||||
| this.navigationDivs['up'].className = this.navigationDivs['up'].className.replace(" active",""); | |||||
| this.navigationDivs['down'].className = this.navigationDivs['down'].className.replace(" active",""); | |||||
| } | |||||
| }, | |||||
| /** | |||||
| * Stop moving in the X direction and unHighlight left and right. | |||||
| * @private | |||||
| */ | |||||
| _xStopMoving : function() { | |||||
| this.xIncrement = 0; | |||||
| if (this.navigationDivs) { | |||||
| this.navigationDivs['left'].className = this.navigationDivs['left'].className.replace(" active",""); | |||||
| this.navigationDivs['right'].className = this.navigationDivs['right'].className.replace(" active",""); | |||||
| } | |||||
| } | |||||
| }; | |||||
| @ -1,552 +0,0 @@ | |||||
| /** | |||||
| * Creation of the SectorMixin var. | |||||
| * | |||||
| * This contains all the functions the Network object can use to employ the sector system. | |||||
| * The sector system is always used by Network, though the benefits only apply to the use of clustering. | |||||
| * If clustering is not used, there is no overhead except for a duplicate object with references to nodes and edges. | |||||
| * | |||||
| * Alex de Mulder | |||||
| * 21-01-2013 | |||||
| */ | |||||
| var SectorMixin = { | |||||
| /** | |||||
| * This function is only called by the setData function of the Network object. | |||||
| * This loads the global references into the active sector. This initializes the sector. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| _putDataInSector : function() { | |||||
| this.sectors["active"][this._sector()].nodes = this.nodes; | |||||
| this.sectors["active"][this._sector()].edges = this.edges; | |||||
| this.sectors["active"][this._sector()].nodeIndices = this.nodeIndices; | |||||
| }, | |||||
| /** | |||||
| * /** | |||||
| * This function sets the global references to nodes, edges and nodeIndices back to | |||||
| * those of the supplied (active) sector. If a type is defined, do the specific type | |||||
| * | |||||
| * @param {String} sectorId | |||||
| * @param {String} [sectorType] | "active" or "frozen" | |||||
| * @private | |||||
| */ | |||||
| _switchToSector : function(sectorId, sectorType) { | |||||
| if (sectorType === undefined || sectorType == "active") { | |||||
| this._switchToActiveSector(sectorId); | |||||
| } | |||||
| else { | |||||
| this._switchToFrozenSector(sectorId); | |||||
| } | |||||
| }, | |||||
| /** | |||||
| * This function sets the global references to nodes, edges and nodeIndices back to | |||||
| * those of the supplied active sector. | |||||
| * | |||||
| * @param sectorId | |||||
| * @private | |||||
| */ | |||||
| _switchToActiveSector : function(sectorId) { | |||||
| this.nodeIndices = this.sectors["active"][sectorId]["nodeIndices"]; | |||||
| this.nodes = this.sectors["active"][sectorId]["nodes"]; | |||||
| this.edges = this.sectors["active"][sectorId]["edges"]; | |||||
| }, | |||||
| /** | |||||
| * This function sets the global references to nodes, edges and nodeIndices back to | |||||
| * those of the supplied active sector. | |||||
| * | |||||
| * @param sectorId | |||||
| * @private | |||||
| */ | |||||
| _switchToSupportSector : function() { | |||||
| this.nodeIndices = this.sectors["support"]["nodeIndices"]; | |||||
| this.nodes = this.sectors["support"]["nodes"]; | |||||
| this.edges = this.sectors["support"]["edges"]; | |||||
| }, | |||||
| /** | |||||
| * This function sets the global references to nodes, edges and nodeIndices back to | |||||
| * those of the supplied frozen sector. | |||||
| * | |||||
| * @param sectorId | |||||
| * @private | |||||
| */ | |||||
| _switchToFrozenSector : function(sectorId) { | |||||
| this.nodeIndices = this.sectors["frozen"][sectorId]["nodeIndices"]; | |||||
| this.nodes = this.sectors["frozen"][sectorId]["nodes"]; | |||||
| this.edges = this.sectors["frozen"][sectorId]["edges"]; | |||||
| }, | |||||
| /** | |||||
| * This function sets the global references to nodes, edges and nodeIndices back to | |||||
| * those of the currently active sector. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| _loadLatestSector : function() { | |||||
| this._switchToSector(this._sector()); | |||||
| }, | |||||
| /** | |||||
| * This function returns the currently active sector Id | |||||
| * | |||||
| * @returns {String} | |||||
| * @private | |||||
| */ | |||||
| _sector : function() { | |||||
| return this.activeSector[this.activeSector.length-1]; | |||||
| }, | |||||
| /** | |||||
| * This function returns the previously active sector Id | |||||
| * | |||||
| * @returns {String} | |||||
| * @private | |||||
| */ | |||||
| _previousSector : function() { | |||||
| if (this.activeSector.length > 1) { | |||||
| return this.activeSector[this.activeSector.length-2]; | |||||
| } | |||||
| else { | |||||
| throw new TypeError('there are not enough sectors in the this.activeSector array.'); | |||||
| } | |||||
| }, | |||||
| /** | |||||
| * We add the active sector at the end of the this.activeSector array | |||||
| * This ensures it is the currently active sector returned by _sector() and it reaches the top | |||||
| * of the activeSector stack. When we reverse our steps we move from the end to the beginning of this stack. | |||||
| * | |||||
| * @param newId | |||||
| * @private | |||||
| */ | |||||
| _setActiveSector : function(newId) { | |||||
| this.activeSector.push(newId); | |||||
| }, | |||||
| /** | |||||
| * We remove the currently active sector id from the active sector stack. This happens when | |||||
| * we reactivate the previously active sector | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| _forgetLastSector : function() { | |||||
| this.activeSector.pop(); | |||||
| }, | |||||
| /** | |||||
| * This function creates a new active sector with the supplied newId. This newId | |||||
| * is the expanding node id. | |||||
| * | |||||
| * @param {String} newId | Id of the new active sector | |||||
| * @private | |||||
| */ | |||||
| _createNewSector : function(newId) { | |||||
| // create the new sector | |||||
| this.sectors["active"][newId] = {"nodes":{}, | |||||
| "edges":{}, | |||||
| "nodeIndices":[], | |||||
| "formationScale": this.scale, | |||||
| "drawingNode": undefined}; | |||||
| // create the new sector render node. This gives visual feedback that you are in a new sector. | |||||
| this.sectors["active"][newId]['drawingNode'] = new Node( | |||||
| {id:newId, | |||||
| color: { | |||||
| background: "#eaefef", | |||||
| border: "495c5e" | |||||
| } | |||||
| },{},{},this.constants); | |||||
| this.sectors["active"][newId]['drawingNode'].clusterSize = 2; | |||||
| }, | |||||
| /** | |||||
| * This function removes the currently active sector. This is called when we create a new | |||||
| * active sector. | |||||
| * | |||||
| * @param {String} sectorId | Id of the active sector that will be removed | |||||
| * @private | |||||
| */ | |||||
| _deleteActiveSector : function(sectorId) { | |||||
| delete this.sectors["active"][sectorId]; | |||||
| }, | |||||
| /** | |||||
| * This function removes the currently active sector. This is called when we reactivate | |||||
| * the previously active sector. | |||||
| * | |||||
| * @param {String} sectorId | Id of the active sector that will be removed | |||||
| * @private | |||||
| */ | |||||
| _deleteFrozenSector : function(sectorId) { | |||||
| delete this.sectors["frozen"][sectorId]; | |||||
| }, | |||||
| /** | |||||
| * Freezing an active sector means moving it from the "active" object to the "frozen" object. | |||||
| * We copy the references, then delete the active entree. | |||||
| * | |||||
| * @param sectorId | |||||
| * @private | |||||
| */ | |||||
| _freezeSector : function(sectorId) { | |||||
| // we move the set references from the active to the frozen stack. | |||||
| this.sectors["frozen"][sectorId] = this.sectors["active"][sectorId]; | |||||
| // we have moved the sector data into the frozen set, we now remove it from the active set | |||||
| this._deleteActiveSector(sectorId); | |||||
| }, | |||||
| /** | |||||
| * This is the reverse operation of _freezeSector. Activating means moving the sector from the "frozen" | |||||
| * object to the "active" object. | |||||
| * | |||||
| * @param sectorId | |||||
| * @private | |||||
| */ | |||||
| _activateSector : function(sectorId) { | |||||
| // we move the set references from the frozen to the active stack. | |||||
| this.sectors["active"][sectorId] = this.sectors["frozen"][sectorId]; | |||||
| // we have moved the sector data into the active set, we now remove it from the frozen stack | |||||
| this._deleteFrozenSector(sectorId); | |||||
| }, | |||||
| /** | |||||
| * This function merges the data from the currently active sector with a frozen sector. This is used | |||||
| * in the process of reverting back to the previously active sector. | |||||
| * The data that is placed in the frozen (the previously active) sector is the node that has been removed from it | |||||
| * upon the creation of a new active sector. | |||||
| * | |||||
| * @param sectorId | |||||
| * @private | |||||
| */ | |||||
| _mergeThisWithFrozen : function(sectorId) { | |||||
| // copy all nodes | |||||
| for (var nodeId in this.nodes) { | |||||
| if (this.nodes.hasOwnProperty(nodeId)) { | |||||
| this.sectors["frozen"][sectorId]["nodes"][nodeId] = this.nodes[nodeId]; | |||||
| } | |||||
| } | |||||
| // copy all edges (if not fully clustered, else there are no edges) | |||||
| for (var edgeId in this.edges) { | |||||
| if (this.edges.hasOwnProperty(edgeId)) { | |||||
| this.sectors["frozen"][sectorId]["edges"][edgeId] = this.edges[edgeId]; | |||||
| } | |||||
| } | |||||
| // merge the nodeIndices | |||||
| for (var i = 0; i < this.nodeIndices.length; i++) { | |||||
| this.sectors["frozen"][sectorId]["nodeIndices"].push(this.nodeIndices[i]); | |||||
| } | |||||
| }, | |||||
| /** | |||||
| * This clusters the sector to one cluster. It was a single cluster before this process started so | |||||
| * we revert to that state. The clusterToFit function with a maximum size of 1 node does this. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| _collapseThisToSingleCluster : function() { | |||||
| this.clusterToFit(1,false); | |||||
| }, | |||||
| /** | |||||
| * We create a new active sector from the node that we want to open. | |||||
| * | |||||
| * @param node | |||||
| * @private | |||||
| */ | |||||
| _addSector : function(node) { | |||||
| // this is the currently active sector | |||||
| var sector = this._sector(); | |||||
| // // this should allow me to select nodes from a frozen set. | |||||
| // if (this.sectors['active'][sector]["nodes"].hasOwnProperty(node.id)) { | |||||
| // console.log("the node is part of the active sector"); | |||||
| // } | |||||
| // else { | |||||
| // console.log("I dont know what the fuck happened!!"); | |||||
| // } | |||||
| // when we switch to a new sector, we remove the node that will be expanded from the current nodes list. | |||||
| delete this.nodes[node.id]; | |||||
| var unqiueIdentifier = util.randomUUID(); | |||||
| // we fully freeze the currently active sector | |||||
| this._freezeSector(sector); | |||||
| // we create a new active sector. This sector has the Id of the node to ensure uniqueness | |||||
| this._createNewSector(unqiueIdentifier); | |||||
| // we add the active sector to the sectors array to be able to revert these steps later on | |||||
| this._setActiveSector(unqiueIdentifier); | |||||
| // we redirect the global references to the new sector's references. this._sector() now returns unqiueIdentifier | |||||
| this._switchToSector(this._sector()); | |||||
| // finally we add the node we removed from our previous active sector to the new active sector | |||||
| this.nodes[node.id] = node; | |||||
| }, | |||||
| /** | |||||
| * We close the sector that is currently open and revert back to the one before. | |||||
| * If the active sector is the "default" sector, nothing happens. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| _collapseSector : function() { | |||||
| // the currently active sector | |||||
| var sector = this._sector(); | |||||
| // we cannot collapse the default sector | |||||
| if (sector != "default") { | |||||
| if ((this.nodeIndices.length == 1) || | |||||
| (this.sectors["active"][sector]["drawingNode"].width*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) || | |||||
| (this.sectors["active"][sector]["drawingNode"].height*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) { | |||||
| var previousSector = this._previousSector(); | |||||
| // we collapse the sector back to a single cluster | |||||
| this._collapseThisToSingleCluster(); | |||||
| // we move the remaining nodes, edges and nodeIndices to the previous sector. | |||||
| // This previous sector is the one we will reactivate | |||||
| this._mergeThisWithFrozen(previousSector); | |||||
| // the previously active (frozen) sector now has all the data from the currently active sector. | |||||
| // we can now delete the active sector. | |||||
| this._deleteActiveSector(sector); | |||||
| // we activate the previously active (and currently frozen) sector. | |||||
| this._activateSector(previousSector); | |||||
| // we load the references from the newly active sector into the global references | |||||
| this._switchToSector(previousSector); | |||||
| // we forget the previously active sector because we reverted to the one before | |||||
| this._forgetLastSector(); | |||||
| // finally, we update the node index list. | |||||
| this._updateNodeIndexList(); | |||||
| // we refresh the list with calulation nodes and calculation node indices. | |||||
| this._updateCalculationNodes(); | |||||
| } | |||||
| } | |||||
| }, | |||||
| /** | |||||
| * This runs a function in all active sectors. This is used in _redraw() and the _initializeForceCalculation(). | |||||
| * | |||||
| * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors | |||||
| * | we dont pass the function itself because then the "this" is the window object | |||||
| * | instead of the Network object | |||||
| * @param {*} [argument] | Optional: arguments to pass to the runFunction | |||||
| * @private | |||||
| */ | |||||
| _doInAllActiveSectors : function(runFunction,argument) { | |||||
| if (argument === undefined) { | |||||
| for (var sector in this.sectors["active"]) { | |||||
| if (this.sectors["active"].hasOwnProperty(sector)) { | |||||
| // switch the global references to those of this sector | |||||
| this._switchToActiveSector(sector); | |||||
| this[runFunction](); | |||||
| } | |||||
| } | |||||
| } | |||||
| else { | |||||
| for (var sector in this.sectors["active"]) { | |||||
| if (this.sectors["active"].hasOwnProperty(sector)) { | |||||
| // switch the global references to those of this sector | |||||
| this._switchToActiveSector(sector); | |||||
| var args = Array.prototype.splice.call(arguments, 1); | |||||
| if (args.length > 1) { | |||||
| this[runFunction](args[0],args[1]); | |||||
| } | |||||
| else { | |||||
| this[runFunction](argument); | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| // we revert the global references back to our active sector | |||||
| this._loadLatestSector(); | |||||
| }, | |||||
| /** | |||||
| * This runs a function in all active sectors. This is used in _redraw() and the _initializeForceCalculation(). | |||||
| * | |||||
| * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors | |||||
| * | we dont pass the function itself because then the "this" is the window object | |||||
| * | instead of the Network object | |||||
| * @param {*} [argument] | Optional: arguments to pass to the runFunction | |||||
| * @private | |||||
| */ | |||||
| _doInSupportSector : function(runFunction,argument) { | |||||
| if (argument === undefined) { | |||||
| this._switchToSupportSector(); | |||||
| this[runFunction](); | |||||
| } | |||||
| else { | |||||
| this._switchToSupportSector(); | |||||
| var args = Array.prototype.splice.call(arguments, 1); | |||||
| if (args.length > 1) { | |||||
| this[runFunction](args[0],args[1]); | |||||
| } | |||||
| else { | |||||
| this[runFunction](argument); | |||||
| } | |||||
| } | |||||
| // we revert the global references back to our active sector | |||||
| this._loadLatestSector(); | |||||
| }, | |||||
| /** | |||||
| * This runs a function in all frozen sectors. This is used in the _redraw(). | |||||
| * | |||||
| * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors | |||||
| * | we don't pass the function itself because then the "this" is the window object | |||||
| * | instead of the Network object | |||||
| * @param {*} [argument] | Optional: arguments to pass to the runFunction | |||||
| * @private | |||||
| */ | |||||
| _doInAllFrozenSectors : function(runFunction,argument) { | |||||
| if (argument === undefined) { | |||||
| for (var sector in this.sectors["frozen"]) { | |||||
| if (this.sectors["frozen"].hasOwnProperty(sector)) { | |||||
| // switch the global references to those of this sector | |||||
| this._switchToFrozenSector(sector); | |||||
| this[runFunction](); | |||||
| } | |||||
| } | |||||
| } | |||||
| else { | |||||
| for (var sector in this.sectors["frozen"]) { | |||||
| if (this.sectors["frozen"].hasOwnProperty(sector)) { | |||||
| // switch the global references to those of this sector | |||||
| this._switchToFrozenSector(sector); | |||||
| var args = Array.prototype.splice.call(arguments, 1); | |||||
| if (args.length > 1) { | |||||
| this[runFunction](args[0],args[1]); | |||||
| } | |||||
| else { | |||||
| this[runFunction](argument); | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| this._loadLatestSector(); | |||||
| }, | |||||
| /** | |||||
| * This runs a function in all sectors. This is used in the _redraw(). | |||||
| * | |||||
| * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors | |||||
| * | we don't pass the function itself because then the "this" is the window object | |||||
| * | instead of the Network object | |||||
| * @param {*} [argument] | Optional: arguments to pass to the runFunction | |||||
| * @private | |||||
| */ | |||||
| _doInAllSectors : function(runFunction,argument) { | |||||
| var args = Array.prototype.splice.call(arguments, 1); | |||||
| if (argument === undefined) { | |||||
| this._doInAllActiveSectors(runFunction); | |||||
| this._doInAllFrozenSectors(runFunction); | |||||
| } | |||||
| else { | |||||
| if (args.length > 1) { | |||||
| this._doInAllActiveSectors(runFunction,args[0],args[1]); | |||||
| this._doInAllFrozenSectors(runFunction,args[0],args[1]); | |||||
| } | |||||
| else { | |||||
| this._doInAllActiveSectors(runFunction,argument); | |||||
| this._doInAllFrozenSectors(runFunction,argument); | |||||
| } | |||||
| } | |||||
| }, | |||||
| /** | |||||
| * This clears the nodeIndices list. We cannot use this.nodeIndices = [] because we would break the link with the | |||||
| * active sector. Thus we clear the nodeIndices in the active sector, then reconnect the this.nodeIndices to it. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| _clearNodeIndexList : function() { | |||||
| var sector = this._sector(); | |||||
| this.sectors["active"][sector]["nodeIndices"] = []; | |||||
| this.nodeIndices = this.sectors["active"][sector]["nodeIndices"]; | |||||
| }, | |||||
| /** | |||||
| * Draw the encompassing sector node | |||||
| * | |||||
| * @param ctx | |||||
| * @param sectorType | |||||
| * @private | |||||
| */ | |||||
| _drawSectorNodes : function(ctx,sectorType) { | |||||
| var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node; | |||||
| for (var sector in this.sectors[sectorType]) { | |||||
| if (this.sectors[sectorType].hasOwnProperty(sector)) { | |||||
| if (this.sectors[sectorType][sector]["drawingNode"] !== undefined) { | |||||
| this._switchToSector(sector,sectorType); | |||||
| minY = 1e9; maxY = -1e9; minX = 1e9; maxX = -1e9; | |||||
| for (var nodeId in this.nodes) { | |||||
| if (this.nodes.hasOwnProperty(nodeId)) { | |||||
| node = this.nodes[nodeId]; | |||||
| node.resize(ctx); | |||||
| if (minX > node.x - 0.5 * node.width) {minX = node.x - 0.5 * node.width;} | |||||
| if (maxX < node.x + 0.5 * node.width) {maxX = node.x + 0.5 * node.width;} | |||||
| if (minY > node.y - 0.5 * node.height) {minY = node.y - 0.5 * node.height;} | |||||
| if (maxY < node.y + 0.5 * node.height) {maxY = node.y + 0.5 * node.height;} | |||||
| } | |||||
| } | |||||
| node = this.sectors[sectorType][sector]["drawingNode"]; | |||||
| node.x = 0.5 * (maxX + minX); | |||||
| node.y = 0.5 * (maxY + minY); | |||||
| node.width = 2 * (node.x - minX); | |||||
| node.height = 2 * (node.y - minY); | |||||
| node.radius = Math.sqrt(Math.pow(0.5*node.width,2) + Math.pow(0.5*node.height,2)); | |||||
| node.setScale(this.scale); | |||||
| node._drawCircle(ctx); | |||||
| } | |||||
| } | |||||
| } | |||||
| }, | |||||
| _drawAllSectorNodes : function(ctx) { | |||||
| this._drawSectorNodes(ctx,"frozen"); | |||||
| this._drawSectorNodes(ctx,"active"); | |||||
| this._loadLatestSector(); | |||||
| } | |||||
| }; | |||||