var Emitter = require('emitter-component');
|
|
var Hammer = require('../module/hammer');
|
|
var util = require('../util');
|
|
var DataSet = require('../DataSet');
|
|
var DataView = require('../DataView');
|
|
var dotparser = require('./dotparser');
|
|
var gephiParser = require('./gephiParser');
|
|
var Groups = require('./Groups');
|
|
var Images = require('./Images');
|
|
var Node = require('./modules/components/nodes/NodeMain');
|
|
var Edge = require('./modules/components/edges/EdgeMain');
|
|
var Popup = require('./Popup');
|
|
var Activator = require('../shared/Activator');
|
|
var locales = require('./locales');
|
|
|
|
// Load custom shapes into CanvasRenderingContext2D
|
|
require('./shapes');
|
|
|
|
import NodesHandler from './modules/NodesHandler';
|
|
import EdgesHandler from './modules/EdgesHandler';
|
|
import PhysicsEngine from './modules/PhysicsEngine';
|
|
import ClusterEngine from './modules/Clustering';
|
|
import CanvasRenderer from './modules/CanvasRenderer';
|
|
import Canvas from './modules/Canvas';
|
|
import View from './modules/View';
|
|
import InteractionHandler from './modules/InteractionHandler';
|
|
import SelectionHandler from "./modules/SelectionHandler";
|
|
import LayoutEngine from "./modules/LayoutEngine";
|
|
|
|
/**
|
|
* @constructor Network
|
|
* Create a network visualization, displaying nodes and edges.
|
|
*
|
|
* @param {Element} container The DOM element in which the Network will
|
|
* be created. Normally a div element.
|
|
* @param {Object} data An object containing parameters
|
|
* {Array} nodes
|
|
* {Array} edges
|
|
* @param {Object} options Options
|
|
*/
|
|
function Network (container, data, options) {
|
|
if (!(this instanceof Network)) {
|
|
throw new SyntaxError('Constructor must be called with the new operator');
|
|
}
|
|
|
|
// set constant values
|
|
this.remainingOptions = {
|
|
dataManipulation: {
|
|
enabled: false,
|
|
initiallyVisible: false
|
|
},
|
|
hierarchicalLayout: {
|
|
enabled:false,
|
|
levelSeparation: 150,
|
|
nodeSpacing: 100,
|
|
direction: "UD", // UD, DU, LR, RL
|
|
layout: "hubsize" // hubsize, directed
|
|
},
|
|
locale: 'en',
|
|
locales: locales,
|
|
useDefaultGroups: true
|
|
};
|
|
|
|
// containers for nodes and edges
|
|
this.body = {
|
|
nodes: {},
|
|
nodeIndices: [],
|
|
supportNodes: {},
|
|
supportNodeIndices: [],
|
|
edges: {},
|
|
data: {
|
|
nodes: null, // A DataSet or DataView
|
|
edges: null // A DataSet or DataView
|
|
},
|
|
functions:{
|
|
createNode: this._createNode.bind(this),
|
|
createEdge: this._createEdge.bind(this)
|
|
},
|
|
emitter: {
|
|
on: this.on.bind(this),
|
|
off: this.off.bind(this),
|
|
emit: this.emit.bind(this),
|
|
once: this.once.bind(this)
|
|
},
|
|
eventListeners: {
|
|
onTap: function() {},
|
|
onTouch: function() {},
|
|
onDoubleTap: function() {},
|
|
onHold: function() {},
|
|
onDragStart: function() {},
|
|
onDrag: function() {},
|
|
onDragEnd: function() {},
|
|
onMouseWheel: function() {},
|
|
onPinch: function() {},
|
|
onMouseMove: function() {},
|
|
onRelease: function() {}
|
|
},
|
|
container: container,
|
|
view: {
|
|
scale:1,
|
|
translation:{x:0,y:0}
|
|
}
|
|
};
|
|
|
|
// todo think of good comment for this set
|
|
var groups = new Groups(); // object with groups
|
|
var images = new Images(() => this.body.emitter.emit("_requestRedraw")); // object with images
|
|
|
|
// data handling modules
|
|
this.canvas = new Canvas(this.body); // DOM handler
|
|
this.selectionHandler = new SelectionHandler(this.body, this.canvas); // Selection handler
|
|
this.interactionHandler = new InteractionHandler(this.body, this.canvas, this.selectionHandler); // Interaction handler handles all the hammer bindings (that are bound by canvas), key
|
|
this.view = new View(this.body, this.canvas); // camera handler, does animations and zooms
|
|
this.renderer = new CanvasRenderer(this.body, this.canvas); // renderer, starts renderloop, has events that modules can hook into
|
|
this.physics = new PhysicsEngine(this.body); // physics engine, does all the simulations
|
|
this.layoutEngine = new LayoutEngine(this.body); // TODO: layout engine for initial positioning and hierarchical positioning
|
|
this.clustering = new ClusterEngine(this.body); // clustering api
|
|
|
|
this.nodesHandler = new NodesHandler(this.body, images, groups, this.layoutEngine); // Handle adding, deleting and updating of nodes as well as global options
|
|
this.edgesHandler = new EdgesHandler(this.body, images, groups); // Handle adding, deleting and updating of edges as well as global options
|
|
|
|
|
|
|
|
// this event will trigger a rebuilding of the cache everything. Used when nodes or edges have been added or removed.
|
|
this.body.emitter.on("_dataChanged", (params) => {
|
|
var t0 = new Date().valueOf();
|
|
// update shortcut lists
|
|
this._updateNodeIndexList();
|
|
this.physics._updateCalculationNodes();
|
|
// update values
|
|
this._updateValueRange(this.body.nodes);
|
|
this._updateValueRange(this.body.edges);
|
|
// update edges
|
|
this._reconnectEdges();
|
|
this.edgesHandler.createBezierNodes(params);
|
|
this._markAllEdgesAsDirty();
|
|
// start simulation (can be called safely, even if already running)
|
|
this.body.emitter.emit("startSimulation");
|
|
console.log("_dataChanged took:", new Date().valueOf() - t0);
|
|
})
|
|
|
|
// this is called when options of EXISTING nodes or edges have changed.
|
|
this.body.emitter.on("_dataUpdated", () => {
|
|
var t0 = new Date().valueOf();
|
|
// update values
|
|
this._updateValueRange(this.body.nodes);
|
|
this._updateValueRange(this.body.edges);
|
|
// update edges
|
|
this._reconnectEdges();
|
|
this.edgesHandler.createBezierNodes(params);
|
|
this._markAllEdgesAsDirty();
|
|
// start simulation (can be called safely, even if already running)
|
|
this.body.emitter.emit("startSimulation");
|
|
console.log("_dataUpdated took:", new Date().valueOf() - t0);
|
|
});
|
|
|
|
// create the DOM elements
|
|
this.canvas.create();
|
|
|
|
// apply options
|
|
this.setOptions(options);
|
|
|
|
// load data (the disable start variable will be the same as the enabled clustering)
|
|
this.setData(data);
|
|
|
|
}
|
|
|
|
// Extend Network with an Emitter mixin
|
|
Emitter(Network.prototype);
|
|
|
|
|
|
Network.prototype._createNode = function(properties) {
|
|
return new Node(properties, this.images, this.groups, this.constants)
|
|
}
|
|
|
|
Network.prototype._createEdge = function(properties) {
|
|
return new Edge(properties, this.body, this.constants)
|
|
}
|
|
|
|
/**
|
|
* Update the this.body.nodeIndices with the most recent node index list
|
|
* @private
|
|
*/
|
|
Network.prototype._updateNodeIndexList = function() {
|
|
this.body.supportNodeIndices = Object.keys(this.body.supportNodes)
|
|
this.body.nodeIndices = Object.keys(this.body.nodes);
|
|
};
|
|
|
|
|
|
/**
|
|
* Set nodes and edges, and optionally options as well.
|
|
*
|
|
* @param {Object} data Object containing parameters:
|
|
* {Array | DataSet | DataView} [nodes] Array with nodes
|
|
* {Array | DataSet | DataView} [edges] Array with edges
|
|
* {String} [dot] String containing data in DOT format
|
|
* {String} [gephi] String containing data in gephi JSON format
|
|
* {Options} [options] Object with options
|
|
* @param {Boolean} [disableStart] | optional: disable the calling of the start function.
|
|
*/
|
|
Network.prototype.setData = function(data) {
|
|
// reset the physics engine.
|
|
this.body.emitter.emit("resetPhysics");
|
|
this.body.emitter.emit("_resetData");
|
|
|
|
// unselect all to ensure no selections from old data are carried over.
|
|
this.selectionHandler.unselectAll();
|
|
|
|
// we set initializing to true to ensure that the hierarchical layout is not performed until both nodes and edges are added.
|
|
this.initializing = true;
|
|
|
|
if (data && data.dot && (data.nodes || data.edges)) {
|
|
throw new SyntaxError('Data must contain either parameter "dot" or ' +
|
|
' parameter pair "nodes" and "edges", but not both.');
|
|
}
|
|
|
|
// clean up in case there is anyone in an active mode of the manipulation. This is the same option as bound to the escape button.
|
|
//if (this.constants.dataManipulation.enabled == true) {
|
|
// this._createManipulatorBar();
|
|
//}
|
|
|
|
// set options
|
|
this.setOptions(data && data.options);
|
|
// set all data
|
|
if (data && data.dot) {
|
|
// parse DOT file
|
|
if(data && data.dot) {
|
|
var dotData = dotparser.DOTToGraph(data.dot);
|
|
this.setData(dotData);
|
|
return;
|
|
}
|
|
}
|
|
else if (data && data.gephi) {
|
|
// parse DOT file
|
|
if(data && data.gephi) {
|
|
var gephiData = gephiParser.parseGephi(data.gephi);
|
|
this.setData(gephiData);
|
|
return;
|
|
}
|
|
}
|
|
else {
|
|
this.nodesHandler.setData(data && data.nodes);
|
|
this.edgesHandler.setData(data && data.edges);
|
|
}
|
|
|
|
// find a stable position or start animating to a stable position
|
|
this.body.emitter.emit("initPhysics");
|
|
};
|
|
|
|
/**
|
|
* Set options
|
|
* @param {Object} options
|
|
*/
|
|
Network.prototype.setOptions = function (options) {
|
|
if (options) {
|
|
//var fields = ['nodes','edges','smoothCurves','hierarchicalLayout','navigation',
|
|
// 'keyboard','dataManipulation','onAdd','onEdit','onEditEdge','onConnect','onDelete','clickToUse'
|
|
//];
|
|
// extend all but the values in fields
|
|
//util.selectiveNotDeepExtend(fields,this.constants, options);
|
|
//util.selectiveNotDeepExtend(['color'],this.constants.nodes, options.nodes);
|
|
//util.selectiveNotDeepExtend(['color','length'],this.constants.edges, options.edges);
|
|
|
|
//this.groups.useDefaultGroups = this.constants.useDefaultGroups;
|
|
|
|
this.nodesHandler.setOptions(options.nodes);
|
|
this.edgesHandler.setOptions(options.edges);
|
|
this.physics.setOptions(options.physics);
|
|
this.canvas.setOptions(options.canvas);
|
|
this.renderer.setOptions(options.rendering);
|
|
this.view.setOptions(options.view);
|
|
this.interactionHandler.setOptions(options.interaction);
|
|
this.selectionHandler.setOptions(options.selection);
|
|
this.layoutEngine.setOptions(options.layout);
|
|
//this.clustering.setOptions(options.clustering);
|
|
|
|
//util.mergeOptions(this.constants, options,'smoothCurves');
|
|
//util.mergeOptions(this.constants, options,'hierarchicalLayout');
|
|
//util.mergeOptions(this.constants, options,'clustering');
|
|
//util.mergeOptions(this.constants, options,'navigation');
|
|
//util.mergeOptions(this.constants, options,'keyboard');
|
|
//util.mergeOptions(this.constants, options,'dataManipulation');
|
|
|
|
|
|
//if (options.dataManipulation) {
|
|
// this.editMode = this.constants.dataManipulation.initiallyVisible;
|
|
//}
|
|
|
|
|
|
// TODO: work out these options and document them
|
|
if (options.edges) {
|
|
if (options.edges.color !== undefined) {
|
|
if (util.isString(options.edges.color)) {
|
|
this.constants.edges.color = {};
|
|
this.constants.edges.color.color = options.edges.color;
|
|
this.constants.edges.color.highlight = options.edges.color;
|
|
this.constants.edges.color.hover = options.edges.color;
|
|
}
|
|
else {
|
|
if (options.edges.color.color !== undefined) {this.constants.edges.color.color = options.edges.color.color;}
|
|
if (options.edges.color.highlight !== undefined) {this.constants.edges.color.highlight = options.edges.color.highlight;}
|
|
if (options.edges.color.hover !== undefined) {this.constants.edges.color.hover = options.edges.color.hover;}
|
|
}
|
|
this.constants.edges.inheritColor = false;
|
|
}
|
|
|
|
if (!options.edges.fontColor) {
|
|
if (options.edges.color !== undefined) {
|
|
if (util.isString(options.edges.color)) {this.constants.edges.fontColor = options.edges.color;}
|
|
else if (options.edges.color.color !== undefined) {this.constants.edges.fontColor = options.edges.color.color;}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (options.nodes) {
|
|
if (options.nodes.color) {
|
|
var newColorObj = util.parseColor(options.nodes.color);
|
|
this.constants.nodes.color.background = newColorObj.background;
|
|
this.constants.nodes.color.border = newColorObj.border;
|
|
this.constants.nodes.color.highlight.background = newColorObj.highlight.background;
|
|
this.constants.nodes.color.highlight.border = newColorObj.highlight.border;
|
|
this.constants.nodes.color.hover.background = newColorObj.hover.background;
|
|
this.constants.nodes.color.hover.border = newColorObj.hover.border;
|
|
}
|
|
}
|
|
if (options.groups) {
|
|
for (var groupname in options.groups) {
|
|
if (options.groups.hasOwnProperty(groupname)) {
|
|
var group = options.groups[groupname];
|
|
this.groups.add(groupname, group);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (options.tooltip) {
|
|
for (prop in options.tooltip) {
|
|
if (options.tooltip.hasOwnProperty(prop)) {
|
|
this.constants.tooltip[prop] = options.tooltip[prop];
|
|
}
|
|
}
|
|
if (options.tooltip.color) {
|
|
this.constants.tooltip.color = util.parseColor(options.tooltip.color);
|
|
}
|
|
}
|
|
|
|
if ('clickToUse' in options) {
|
|
if (options.clickToUse === true) {
|
|
if (this.activator === undefined) {
|
|
this.activator = new Activator(this.frame);
|
|
this.activator.on('change', this._createKeyBinds.bind(this));
|
|
}
|
|
}
|
|
else {
|
|
if (this.activator !== undefined) {
|
|
this.activator.destroy();
|
|
delete this.activator;
|
|
}
|
|
this.body.emitter.emit("activate");
|
|
}
|
|
}
|
|
else {
|
|
this.body.emitter.emit("activate");
|
|
}
|
|
|
|
this.canvas.setSize();
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Cleans up all bindings of the network, removing it fully from the memory IF the variable is set to null after calling this function.
|
|
* var network = new vis.Network(..);
|
|
* network.destroy();
|
|
* network = null;
|
|
*/
|
|
Network.prototype.destroy = function() {
|
|
this.body.emitter.emit("destroy");
|
|
|
|
// clear events
|
|
this.body.emitter.off();
|
|
|
|
// remove the container and everything inside it recursively
|
|
this.util.recursiveDOMDelete(this.body.container);
|
|
};
|
|
|
|
|
|
/**
|
|
* Check if there is an element on the given position in the network
|
|
* (a node or edge). If so, and if this element has a title,
|
|
* show a popup window with its title.
|
|
*
|
|
* @param {{x:Number, y:Number}} pointer
|
|
* @private
|
|
*/
|
|
Network.prototype._checkShowPopup = function (pointer) {
|
|
var obj = {
|
|
left: this._XconvertDOMtoCanvas(pointer.x),
|
|
top: this._YconvertDOMtoCanvas(pointer.y),
|
|
right: this._XconvertDOMtoCanvas(pointer.x),
|
|
bottom: this._YconvertDOMtoCanvas(pointer.y)
|
|
};
|
|
|
|
var id;
|
|
var previousPopupObjId = this.popupObj === undefined ? "" : this.popupObj.id;
|
|
var nodeUnderCursor = false;
|
|
var popupType = "node";
|
|
|
|
if (this.popupObj == undefined) {
|
|
// search the nodes for overlap, select the top one in case of multiple nodes
|
|
var nodes = this.body.nodes;
|
|
var overlappingNodes = [];
|
|
for (id in nodes) {
|
|
if (nodes.hasOwnProperty(id)) {
|
|
var node = nodes[id];
|
|
if (node.isOverlappingWith(obj)) {
|
|
if (node.getTitle() !== undefined) {
|
|
overlappingNodes.push(id);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (overlappingNodes.length > 0) {
|
|
// if there are overlapping nodes, select the last one, this is the
|
|
// one which is drawn on top of the others
|
|
this.popupObj = this.body.nodes[overlappingNodes[overlappingNodes.length - 1]];
|
|
// if you hover over a node, the title of the edge is not supposed to be shown.
|
|
nodeUnderCursor = true;
|
|
}
|
|
}
|
|
|
|
if (this.popupObj === undefined && nodeUnderCursor == false) {
|
|
// search the edges for overlap
|
|
var edges = this.body.edges;
|
|
var overlappingEdges = [];
|
|
for (id in edges) {
|
|
if (edges.hasOwnProperty(id)) {
|
|
var edge = edges[id];
|
|
if (edge.connected === true && (edge.getTitle() !== undefined) &&
|
|
edge.isOverlappingWith(obj)) {
|
|
overlappingEdges.push(id);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (overlappingEdges.length > 0) {
|
|
this.popupObj = this.body.edges[overlappingEdges[overlappingEdges.length - 1]];
|
|
popupType = "edge";
|
|
}
|
|
}
|
|
|
|
if (this.popupObj) {
|
|
// show popup message window
|
|
if (this.popupObj.id != previousPopupObjId) {
|
|
if (this.popup === undefined) {
|
|
this.popup = new Popup(this.frame, this.constants.tooltip);
|
|
}
|
|
|
|
this.popup.popupTargetType = popupType;
|
|
this.popup.popupTargetId = this.popupObj.id;
|
|
|
|
// adjust a small offset such that the mouse cursor is located in the
|
|
// bottom left location of the popup, and you can easily move over the
|
|
// popup area
|
|
this.popup.setPosition(pointer.x + 3, pointer.y - 5);
|
|
this.popup.setText(this.popupObj.getTitle());
|
|
this.popup.show();
|
|
}
|
|
}
|
|
else {
|
|
if (this.popup) {
|
|
this.popup.hide();
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Check if the popup must be hidden, which is the case when the mouse is no
|
|
* longer hovering on the object
|
|
* @param {{x:Number, y:Number}} pointer
|
|
* @private
|
|
*/
|
|
Network.prototype._checkHidePopup = function (pointer) {
|
|
var pointerObj = {
|
|
left: this._XconvertDOMtoCanvas(pointer.x),
|
|
top: this._YconvertDOMtoCanvas(pointer.y),
|
|
right: this._XconvertDOMtoCanvas(pointer.x),
|
|
bottom: this._YconvertDOMtoCanvas(pointer.y)
|
|
};
|
|
|
|
var stillOnObj = false;
|
|
if (this.popup.popupTargetType == 'node') {
|
|
stillOnObj = this.body.nodes[this.popup.popupTargetId].isOverlappingWith(pointerObj);
|
|
if (stillOnObj === true) {
|
|
var overNode = this.getNodeAt(pointer);
|
|
stillOnObj = overNode.id == this.popup.popupTargetId;
|
|
}
|
|
}
|
|
else {
|
|
if (this.getNodeAt(pointer) === null) {
|
|
stillOnObj = this.body.edges[this.popup.popupTargetId].isOverlappingWith(pointerObj);
|
|
}
|
|
}
|
|
|
|
|
|
if (stillOnObj === false) {
|
|
this.popupObj = undefined;
|
|
this.popup.hide();
|
|
}
|
|
};
|
|
|
|
|
|
|
|
|
|
Network.prototype._markAllEdgesAsDirty = function() {
|
|
for (var edgeId in this.body.edges) {
|
|
this.body.edges[edgeId].colorDirty = true;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Reconnect all edges
|
|
* @private
|
|
*/
|
|
Network.prototype._reconnectEdges = function() {
|
|
var id,
|
|
nodes = this.body.nodes,
|
|
edges = this.body.edges;
|
|
for (id in nodes) {
|
|
if (nodes.hasOwnProperty(id)) {
|
|
nodes[id].edges = [];
|
|
}
|
|
}
|
|
|
|
for (id in edges) {
|
|
if (edges.hasOwnProperty(id)) {
|
|
var edge = edges[id];
|
|
edge.from = null;
|
|
edge.to = null;
|
|
edge.connect();
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Update the values of all object in the given array according to the current
|
|
* value range of the objects in the array.
|
|
* @param {Object} obj An object containing a set of Edges or Nodes
|
|
* The objects must have a method getValue() and
|
|
* setValueRange(min, max).
|
|
* @private
|
|
*/
|
|
Network.prototype._updateValueRange = function(obj) {
|
|
var id;
|
|
|
|
// determine the range of the objects
|
|
var valueMin = undefined;
|
|
var valueMax = undefined;
|
|
var valueTotal = 0;
|
|
for (id in obj) {
|
|
if (obj.hasOwnProperty(id)) {
|
|
var value = obj[id].getValue();
|
|
if (value !== undefined) {
|
|
valueMin = (valueMin === undefined) ? value : Math.min(value, valueMin);
|
|
valueMax = (valueMax === undefined) ? value : Math.max(value, valueMax);
|
|
valueTotal += value;
|
|
}
|
|
}
|
|
}
|
|
|
|
// adjust the range of all objects
|
|
if (valueMin !== undefined && valueMax !== undefined) {
|
|
for (id in obj) {
|
|
if (obj.hasOwnProperty(id)) {
|
|
obj[id].setValueRange(valueMin, valueMax, valueTotal);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Set the translation of the network
|
|
* @param {Number} offsetX Horizontal offset
|
|
* @param {Number} offsetY Vertical offset
|
|
* @private
|
|
*/
|
|
Network.prototype._setTranslation = function(offsetX, offsetY) {
|
|
if (this.translation === undefined) {
|
|
this.translation = {
|
|
x: 0,
|
|
y: 0
|
|
};
|
|
}
|
|
|
|
if (offsetX !== undefined) {
|
|
this.translation.x = offsetX;
|
|
}
|
|
if (offsetY !== undefined) {
|
|
this.translation.y = offsetY;
|
|
}
|
|
|
|
this.emit('viewChanged');
|
|
};
|
|
|
|
/**
|
|
* Get the translation of the network
|
|
* @return {Object} translation An object with parameters x and y, both a number
|
|
* @private
|
|
*/
|
|
Network.prototype._getTranslation = function() {
|
|
return {
|
|
x: this.translation.x,
|
|
y: this.translation.y
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Scale the network
|
|
* @param {Number} scale Scaling factor 1.0 is unscaled
|
|
* @private
|
|
*/
|
|
Network.prototype._setScale = function(scale) {
|
|
this.scale = scale;
|
|
};
|
|
|
|
/**
|
|
* Get the current scale of the network
|
|
* @return {Number} scale Scaling factor 1.0 is unscaled
|
|
* @private
|
|
*/
|
|
Network.prototype._getScale = function() {
|
|
return this.scale;
|
|
};
|
|
|
|
|
|
/**
|
|
* Move the network according to the keyboard presses.
|
|
*
|
|
* @private
|
|
*/
|
|
Network.prototype._handleNavigation = function() {
|
|
if (this.xIncrement != 0 || this.yIncrement != 0) {
|
|
var translation = this._getTranslation();
|
|
this._setTranslation(translation.x+this.xIncrement, translation.y+this.yIncrement);
|
|
}
|
|
if (this.zoomIncrement != 0) {
|
|
var center = {
|
|
x: this.frame.canvas.clientWidth / 2,
|
|
y: this.frame.canvas.clientHeight / 2
|
|
};
|
|
this.zoom(this.scale*(1 + this.zoomIncrement), center);
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Freeze the _animationStep
|
|
*/
|
|
Network.prototype.freezeSimulation = function(freeze) {
|
|
if (freeze == true) {
|
|
this.freezeSimulationEnabled = true;
|
|
this.moving = false;
|
|
}
|
|
else {
|
|
this.freezeSimulationEnabled = false;
|
|
this.moving = true;
|
|
this.start();
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* This function cleans the support nodes if they are not needed and adds them when they are.
|
|
*
|
|
* @param {boolean} [disableStart]
|
|
* @private
|
|
*/
|
|
Network.prototype._configureSmoothCurves = function(disableStart = true) {
|
|
if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) {
|
|
this._createBezierNodes();
|
|
// cleanup unused support nodes
|
|
for (let i = 0; i < this.body.supportNodeIndices.length; i++) {
|
|
let nodeId = this.body.supportNodeIndices[i];
|
|
// delete support nodes for edges that have been deleted
|
|
if (this.body.edges[this.body.supportNodes[nodeId].parentEdgeId] === undefined) {
|
|
delete this.body.supportNodes[nodeId];
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
// delete the support nodes
|
|
this.body.supportNodes = {};
|
|
for (var edgeId in this.body.edges) {
|
|
if (this.body.edges.hasOwnProperty(edgeId)) {
|
|
this.body.edges[edgeId].via = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
this._updateNodeIndexList();
|
|
this.physics._updateCalculationNodes();
|
|
if (!disableStart) {
|
|
this.moving = true;
|
|
this.start();
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* load the functions that load the mixins into the prototype.
|
|
*
|
|
* @private
|
|
*/
|
|
Network.prototype._initializeMixinLoaders = function () {
|
|
for (var mixin in MixinLoader) {
|
|
if (MixinLoader.hasOwnProperty(mixin)) {
|
|
Network.prototype[mixin] = MixinLoader[mixin];
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Load the XY positions of the nodes into the dataset.
|
|
*/
|
|
Network.prototype.storePosition = function() {
|
|
console.log("storePosition is depricated: use .storePositions() from now on.")
|
|
this.storePositions();
|
|
};
|
|
|
|
/**
|
|
* Load the XY positions of the nodes into the dataset.
|
|
*/
|
|
Network.prototype.storePositions = function() {
|
|
var dataArray = [];
|
|
for (var nodeId in this.body.nodes) {
|
|
if (this.body.nodes.hasOwnProperty(nodeId)) {
|
|
var node = this.body.nodes[nodeId];
|
|
var allowedToMoveX = !this.body.nodes.xFixed;
|
|
var allowedToMoveY = !this.body.nodes.yFixed;
|
|
if (this.body.data.nodes._data[nodeId].x != Math.round(node.x) || this.body.data.nodes._data[nodeId].y != Math.round(node.y)) {
|
|
dataArray.push({id:nodeId,x:Math.round(node.x),y:Math.round(node.y),allowedToMoveX:allowedToMoveX,allowedToMoveY:allowedToMoveY});
|
|
}
|
|
}
|
|
}
|
|
this.body.data.nodes.update(dataArray);
|
|
};
|
|
|
|
/**
|
|
* Return the positions of the nodes.
|
|
*/
|
|
Network.prototype.getPositions = function(ids) {
|
|
var dataArray = {};
|
|
if (ids !== undefined) {
|
|
if (Array.isArray(ids) == true) {
|
|
for (var i = 0; i < ids.length; i++) {
|
|
if (this.body.nodes[ids[i]] !== undefined) {
|
|
var node = this.body.nodes[ids[i]];
|
|
dataArray[ids[i]] = {x: Math.round(node.x), y: Math.round(node.y)};
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
if (this.body.nodes[ids] !== undefined) {
|
|
var node = this.body.nodes[ids];
|
|
dataArray[ids] = {x: Math.round(node.x), y: Math.round(node.y)};
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
for (var nodeId in this.body.nodes) {
|
|
if (this.body.nodes.hasOwnProperty(nodeId)) {
|
|
var node = this.body.nodes[nodeId];
|
|
dataArray[nodeId] = {x: Math.round(node.x), y: Math.round(node.y)};
|
|
}
|
|
}
|
|
}
|
|
return dataArray;
|
|
};
|
|
|
|
|
|
/**
|
|
* Returns true when the Network is active.
|
|
* @returns {boolean}
|
|
*/
|
|
Network.prototype.isActive = function () {
|
|
return !this.activator || this.activator.active;
|
|
};
|
|
|
|
|
|
/**
|
|
* Sets the scale
|
|
* @returns {Number}
|
|
*/
|
|
Network.prototype.setScale = function () {
|
|
return this._setScale();
|
|
};
|
|
|
|
|
|
/**
|
|
* Returns the scale
|
|
* @returns {Number}
|
|
*/
|
|
Network.prototype.getScale = function () {
|
|
return this._getScale();
|
|
};
|
|
|
|
|
|
/**
|
|
* Check if a node is a cluster.
|
|
* @param nodeId
|
|
* @returns {*}
|
|
*/
|
|
Network.prototype.isCluster = function(nodeId) {
|
|
if (this.body.nodes[nodeId] !== undefined) {
|
|
return this.body.nodes[nodeId].isCluster;
|
|
}
|
|
else {
|
|
console.log("Node does not exist.")
|
|
return false;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Returns the scale
|
|
* @returns {Number}
|
|
*/
|
|
Network.prototype.getCenterCoordinates = function () {
|
|
return this.DOMtoCanvas({x: 0.5 * this.frame.canvas.clientWidth, y: 0.5 * this.frame.canvas.clientHeight});
|
|
};
|
|
|
|
|
|
Network.prototype.getBoundingBox = function(nodeId) {
|
|
if (this.body.nodes[nodeId] !== undefined) {
|
|
return this.body.nodes[nodeId].boundingBox;
|
|
}
|
|
}
|
|
|
|
Network.prototype.getConnectedNodes = function(nodeId) {
|
|
var nodeList = [];
|
|
if (this.body.nodes[nodeId] !== undefined) {
|
|
var node = this.body.nodes[nodeId];
|
|
var nodeObj = {nodeId : true}; // used to quickly check if node already exists
|
|
for (var i = 0; i < node.edges.length; i++) {
|
|
var edge = node.edges[i];
|
|
if (edge.toId == nodeId) {
|
|
if (nodeObj[edge.fromId] === undefined) {
|
|
nodeList.push(edge.fromId);
|
|
nodeObj[edge.fromId] = true;
|
|
}
|
|
}
|
|
else if (edge.fromId == nodeId) {
|
|
if (nodeObj[edge.toId] === undefined) {
|
|
nodeList.push(edge.toId)
|
|
nodeObj[edge.toId] = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nodeList;
|
|
}
|
|
|
|
|
|
Network.prototype.getEdgesFromNode = function(nodeId) {
|
|
var edgesList = [];
|
|
if (this.body.nodes[nodeId] !== undefined) {
|
|
var node = this.body.nodes[nodeId];
|
|
for (var i = 0; i < node.edges.length; i++) {
|
|
edgesList.push(node.edges[i].id);
|
|
}
|
|
}
|
|
return edgesList;
|
|
}
|
|
|
|
Network.prototype.generateColorObject = function(color) {
|
|
return util.parseColor(color);
|
|
|
|
}
|
|
|
|
module.exports = Network;
|