var util = require('../../../util');
|
|
|
|
|
|
import Label from './unified/label.js'
|
|
import BezierEdgeDynamic from './edges/bezierEdgeDynamic'
|
|
import BezierEdgeStatic from './edges/bezierEdgeStatic'
|
|
import StraightEdge from './edges/straightEdge'
|
|
/**
|
|
* @class Edge
|
|
*
|
|
* A edge connects two nodes
|
|
* @param {Object} properties Object with options. Must contain
|
|
* At least options from and to.
|
|
* Available options: from (number),
|
|
* to (number), label (string, color (string),
|
|
* width (number), style (string),
|
|
* length (number), title (string)
|
|
* @param {Network} network A Network object, used to find and edge to
|
|
* nodes.
|
|
* @param {Object} constants An object with default values for
|
|
* example for the color
|
|
*/
|
|
class Edge {
|
|
constructor(options, body, globalOptions) {
|
|
if (body === undefined) {
|
|
throw "No body provided";
|
|
}
|
|
this.options = util.bridgeObject(globalOptions);
|
|
this.body = body;
|
|
|
|
// initialize variables
|
|
this.id = undefined;
|
|
this.fromId = undefined;
|
|
this.toId = undefined;
|
|
this.title = undefined;
|
|
this.value = undefined;
|
|
this.selected = false;
|
|
this.hover = false;
|
|
this.labelDirty = true;
|
|
this.colorDirty = true;
|
|
|
|
this.from = undefined; // a node
|
|
this.to = undefined; // a node
|
|
|
|
this.edgeType = undefined;
|
|
|
|
this.connected = false;
|
|
|
|
this.labelModule = new Label(this.body, this.options);
|
|
|
|
this.setOptions(options);
|
|
|
|
this.controlNodesEnabled = false;
|
|
this.controlNodes = {from: undefined, to: undefined, positions: {}};
|
|
this.connectedNode = undefined;
|
|
}
|
|
|
|
|
|
/**
|
|
* Set or overwrite options for the edge
|
|
* @param {Object} options an object with options
|
|
* @param doNotEmit
|
|
*/
|
|
setOptions(options) {
|
|
if (!options) {
|
|
return;
|
|
}
|
|
this.colorDirty = true;
|
|
|
|
var fields = [
|
|
'id',
|
|
'font',
|
|
'from',
|
|
'hidden',
|
|
'hoverWidth',
|
|
'label',
|
|
'length',
|
|
'line',
|
|
'opacity',
|
|
'physics',
|
|
'scaling',
|
|
'selfReferenceSize',
|
|
'to',
|
|
'value',
|
|
'width',
|
|
'widthMin',
|
|
'widthMax',
|
|
'widthSelectionMultiplier'
|
|
];
|
|
util.selectiveDeepExtend(fields, this.options, options);
|
|
|
|
util.mergeOptions(this.options, options, 'smooth');
|
|
util.mergeOptions(this.options, options, 'dashes');
|
|
|
|
if (options.arrows !== undefined) {
|
|
if (typeof options.arrows === 'string') {
|
|
let arrows = options.arrows.toLowerCase();
|
|
if (arrows.indexOf("to") != -1) {this.options.arrows.to.enabled = true;}
|
|
if (arrows.indexOf("middle") != -1) {this.options.arrows.middle.enabled = true;}
|
|
if (arrows.indexOf("from") != -1) {this.options.arrows.from.enabled = true;}
|
|
}
|
|
else if (typeof options.arrows === 'object') {
|
|
util.mergeOptions(this.options.arrows, options.arrows, 'to');
|
|
util.mergeOptions(this.options.arrows, options.arrows, 'middle');
|
|
util.mergeOptions(this.options.arrows, options.arrows, 'from');
|
|
}
|
|
else {
|
|
throw new Error("The arrow options can only be an object or a string. Refer to the documentation. You used:" + JSON.stringify(options.arrows));
|
|
}
|
|
}
|
|
|
|
if (options.id !== undefined) {this.id = options.id;}
|
|
if (options.from !== undefined) {this.fromId = options.from;}
|
|
if (options.to !== undefined) {this.toId = options.to;}
|
|
if (options.title !== undefined) {this.title = options.title;}
|
|
if (options.value !== undefined) {this.value = options.value;}
|
|
|
|
if (options.color !== undefined) {
|
|
if (util.isString(options.color)) {
|
|
this.options.color.color = options.color;
|
|
this.options.color.highlight = options.color;
|
|
}
|
|
else {
|
|
if (options.color.color !== undefined) {
|
|
this.options.color.color = options.color.color;
|
|
}
|
|
if (options.color.highlight !== undefined) {
|
|
this.options.color.highlight = options.color.highlight;
|
|
}
|
|
if (options.color.hover !== undefined) {
|
|
this.options.color.hover = options.color.hover;
|
|
}
|
|
}
|
|
|
|
// inherit colors
|
|
if (options.color.inherit === undefined) {
|
|
this.options.color.inherit.enabled = false;
|
|
}
|
|
else {
|
|
util.mergeOptions(this.options.color, options.color, 'inherit');
|
|
}
|
|
}
|
|
|
|
// A node is connected when it has a from and to node that both exist in the network.body.nodes.
|
|
this.connect();
|
|
|
|
this.labelModule.setOptions(this.options);
|
|
|
|
this.updateEdgeType();
|
|
|
|
return this.edgeType.setOptions(this.options);
|
|
}
|
|
|
|
updateEdgeType() {
|
|
let dataChanged = false;
|
|
let changeInType = true;
|
|
if (this.edgeType !== undefined) {
|
|
if (this.edgeType instanceof BezierEdgeDynamic && this.options.smooth.enabled == true && this.options.smooth.dynamic == true) {changeInType = false;}
|
|
if (this.edgeType instanceof BezierEdgeStatic && this.options.smooth.enabled == true && this.options.smooth.dynamic == false){changeInType = false;}
|
|
if (this.edgeType instanceof StraightEdge && this.options.smooth.enabled == false) {changeInType = false;}
|
|
|
|
if (changeInType == true) {
|
|
dataChanged = this.edgeType.cleanup();
|
|
}
|
|
}
|
|
|
|
if (changeInType === true) {
|
|
if (this.options.smooth.enabled === true) {
|
|
if (this.options.smooth.dynamic === true) {
|
|
dataChanged = true;
|
|
this.edgeType = new BezierEdgeDynamic(this.options, this.body, this.labelModule);
|
|
}
|
|
else {
|
|
this.edgeType = new BezierEdgeStatic(this.options, this.body, this.labelModule);
|
|
}
|
|
}
|
|
else {
|
|
this.edgeType = new StraightEdge(this.options, this.body, this.labelModule);
|
|
}
|
|
}
|
|
|
|
return dataChanged;
|
|
}
|
|
|
|
|
|
/**
|
|
* Enable or disable the physics.
|
|
* @param status
|
|
*/
|
|
togglePhysics(status) {
|
|
if (this.options.smooth.enabled == true && this.options.smooth.dynamic == true) {
|
|
if (this.via === undefined) {
|
|
this.via.pptions.physics = status;
|
|
}
|
|
}
|
|
this.options.physics = status;
|
|
}
|
|
|
|
/**
|
|
* Connect an edge to its nodes
|
|
*/
|
|
connect() {
|
|
this.disconnect();
|
|
|
|
this.from = this.body.nodes[this.fromId] || undefined;
|
|
this.to = this.body.nodes[this.toId] || undefined;
|
|
this.connected = (this.from !== undefined && this.to !== undefined);
|
|
|
|
if (this.connected === true) {
|
|
this.from.attachEdge(this);
|
|
this.to.attachEdge(this);
|
|
}
|
|
else {
|
|
if (this.from) {
|
|
this.from.detachEdge(this);
|
|
}
|
|
if (this.to) {
|
|
this.to.detachEdge(this);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Disconnect an edge from its nodes
|
|
*/
|
|
disconnect() {
|
|
if (this.from) {
|
|
this.from.detachEdge(this);
|
|
this.from = undefined;
|
|
}
|
|
if (this.to) {
|
|
this.to.detachEdge(this);
|
|
this.to = undefined;
|
|
}
|
|
|
|
this.connected = false;
|
|
}
|
|
|
|
|
|
/**
|
|
* get the title of this edge.
|
|
* @return {string} title The title of the edge, or undefined when no title
|
|
* has been set.
|
|
*/
|
|
getTitle() {
|
|
return typeof this.title === "function" ? this.title() : this.title;
|
|
}
|
|
|
|
|
|
/**
|
|
* check if this node is selecte
|
|
* @return {boolean} selected True if node is selected, else false
|
|
*/
|
|
isSelected() {
|
|
return this.selected;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Retrieve the value of the edge. Can be undefined
|
|
* @return {Number} value
|
|
*/
|
|
getValue() {
|
|
return this.value;
|
|
}
|
|
|
|
|
|
/**
|
|
* Adjust the value range of the edge. The edge will adjust it's width
|
|
* based on its value.
|
|
* @param {Number} min
|
|
* @param {Number} max
|
|
* @param total
|
|
*/
|
|
setValueRange(min, max, total) {
|
|
if (this.value !== undefined) {
|
|
var scale = this.options.scaling.customScalingFunction(min, max, total, this.value);
|
|
var widthDiff = this.options.scaling.max - this.options.scaling.min;
|
|
if (this.options.scaling.label.enabled == true) {
|
|
var fontDiff = this.options.scaling.label.max - this.options.scaling.label.min;
|
|
this.options.font.size = this.options.scaling.label.min + scale * fontDiff;
|
|
}
|
|
this.options.width = this.options.scaling.min + scale * widthDiff;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Redraw a edge
|
|
* Draw this edge in the given canvas
|
|
* The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
|
|
* @param {CanvasRenderingContext2D} ctx
|
|
*/
|
|
draw(ctx) {
|
|
let via = this.edgeType.drawLine(ctx, this.selected, this.hover);
|
|
this.drawArrows(ctx, via);
|
|
this.drawLabel (ctx, via);
|
|
}
|
|
|
|
drawArrows(ctx, viaNode) {
|
|
if (this.options.arrows.from.enabled === true) {this.edgeType.drawArrowHead(ctx,'from', viaNode);}
|
|
if (this.options.arrows.middle.enabled === true) {this.edgeType.drawArrowHead(ctx,'middle', viaNode);}
|
|
if (this.options.arrows.to.enabled === true) {this.edgeType.drawArrowHead(ctx,'to', viaNode);}
|
|
}
|
|
|
|
drawLabel(ctx, viaNode) {
|
|
if (this.options.label !== undefined) {
|
|
// set style
|
|
var node1 = this.from;
|
|
var node2 = this.to;
|
|
var selected = (this.from.selected || this.to.selected || this.selected);
|
|
if (node1.id != node2.id) {
|
|
var point = this.edgeType.getPoint(0.5, viaNode);
|
|
ctx.save();
|
|
|
|
// if the label has to be rotated:
|
|
if (this.options.font.align !== "horizontal") {
|
|
this.labelModule.calculateLabelSize(ctx,selected,point.x,point.y);
|
|
ctx.translate(point.x, this.labelModule.size.yLine);
|
|
this._rotateForLabelAlignment(ctx);
|
|
}
|
|
|
|
// draw the label
|
|
this.labelModule.draw(ctx, point.x, point.y, selected);
|
|
ctx.restore();
|
|
}
|
|
else {
|
|
var x, y;
|
|
var radius = this.options.selfReferenceSize;
|
|
if (node1.width > node1.height) {
|
|
x = node1.x + node1.width * 0.5;
|
|
y = node1.y - radius;
|
|
}
|
|
else {
|
|
x = node1.x + radius;
|
|
y = node1.y - node1.height * 0.5;
|
|
}
|
|
point = this._pointOnCircle(x, y, radius, 0.125);
|
|
|
|
this.labelModule.draw(ctx, point.x, point.y, selected);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Check if this object is overlapping with the provided object
|
|
* @param {Object} obj an object with parameters left, top
|
|
* @return {boolean} True if location is located on the edge
|
|
*/
|
|
isOverlappingWith(obj) {
|
|
if (this.connected) {
|
|
var distMax = 10;
|
|
var xFrom = this.from.x;
|
|
var yFrom = this.from.y;
|
|
var xTo = this.to.x;
|
|
var yTo = this.to.y;
|
|
var xObj = obj.left;
|
|
var yObj = obj.top;
|
|
|
|
var dist = this.edgeType.getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj);
|
|
|
|
return (dist < distMax);
|
|
}
|
|
else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Rotates the canvas so the text is most readable
|
|
* @param {CanvasRenderingContext2D} ctx
|
|
* @private
|
|
*/
|
|
_rotateForLabelAlignment(ctx) {
|
|
var dy = this.from.y - this.to.y;
|
|
var dx = this.from.x - this.to.x;
|
|
var angleInDegrees = Math.atan2(dy, dx);
|
|
|
|
// rotate so label it is readable
|
|
if ((angleInDegrees < -1 && dx < 0) || (angleInDegrees > 0 && dx < 0)) {
|
|
angleInDegrees = angleInDegrees + Math.PI;
|
|
}
|
|
|
|
ctx.rotate(angleInDegrees);
|
|
}
|
|
|
|
|
|
/**
|
|
* Get a point on a circle
|
|
* @param {Number} x
|
|
* @param {Number} y
|
|
* @param {Number} radius
|
|
* @param {Number} percentage. Value between 0 (line start) and 1 (line end)
|
|
* @return {Object} point
|
|
* @private
|
|
*/
|
|
_pointOnCircle(x, y, radius, percentage) {
|
|
var angle = percentage * 2 * Math.PI;
|
|
return {
|
|
x: x + radius * Math.cos(angle),
|
|
y: y - radius * Math.sin(angle)
|
|
}
|
|
}
|
|
|
|
|
|
select() {
|
|
this.selected = true;
|
|
}
|
|
|
|
|
|
unselect() {
|
|
this.selected = false;
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//*************************************************************************************************//
|
|
//*************************************************************************************************//
|
|
//*************************************************************************************************//
|
|
//*************************************************************************************************//
|
|
//*********************** MOVE THESE FUNCTIONS TO THE MANIPULATION SYSTEM ************************//
|
|
//*************************************************************************************************//
|
|
//*************************************************************************************************//
|
|
//*************************************************************************************************//
|
|
//*************************************************************************************************//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
* This function draws the control nodes for the manipulator.
|
|
* In order to enable this, only set the this.controlNodesEnabled to true.
|
|
* @param ctx
|
|
*/
|
|
_drawControlNodes(ctx) {
|
|
if (this.controlNodesEnabled == true) {
|
|
if (this.controlNodes.from === undefined && this.controlNodes.to === undefined) {
|
|
var nodeIdFrom = "edgeIdFrom:".concat(this.id);
|
|
var nodeIdTo = "edgeIdTo:".concat(this.id);
|
|
var nodeFromOptions = {
|
|
id: nodeIdFrom,
|
|
shape: 'dot',
|
|
color: {background: '#ff0000', border: '#3c3c3c', highlight: {background: '#07f968'}},
|
|
radius: 7,
|
|
borderWidth: 2,
|
|
borderWidthSelected: 2,
|
|
hidden: false,
|
|
physics: false
|
|
};
|
|
var nodeToOptions = util.deepExtend({},nodeFromOptions);
|
|
nodeToOptions.id = nodeIdTo;
|
|
|
|
|
|
this.controlNodes.from = this.body.functions.createNode(nodeFromOptions);
|
|
this.controlNodes.to = this.body.functions.createNode(nodeToOptions);
|
|
}
|
|
|
|
this.controlNodes.positions = {};
|
|
if (this.controlNodes.from.selected == false) {
|
|
this.controlNodes.positions.from = this.getControlNodeFromPosition(ctx);
|
|
this.controlNodes.from.x = this.controlNodes.positions.from.x;
|
|
this.controlNodes.from.y = this.controlNodes.positions.from.y;
|
|
}
|
|
if (this.controlNodes.to.selected == false) {
|
|
this.controlNodes.positions.to = this.getControlNodeToPosition(ctx);
|
|
this.controlNodes.to.x = this.controlNodes.positions.to.x;
|
|
this.controlNodes.to.y = this.controlNodes.positions.to.y;
|
|
}
|
|
|
|
this.controlNodes.from.draw(ctx);
|
|
this.controlNodes.to.draw(ctx);
|
|
}
|
|
else {
|
|
this.controlNodes = {from: undefined, to: undefined, positions: {}};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Enable control nodes.
|
|
* @private
|
|
*/
|
|
_enableControlNodes() {
|
|
this.fromBackup = this.from;
|
|
this.toBackup = this.to;
|
|
this.controlNodesEnabled = true;
|
|
}
|
|
|
|
|
|
/**
|
|
* disable control nodes and remove from dynamicEdges from old node
|
|
* @private
|
|
*/
|
|
_disableControlNodes() {
|
|
this.fromId = this.from.id;
|
|
this.toId = this.to.id;
|
|
if (this.fromId != this.fromBackup.id) { // from was changed, remove edge from old 'from' node dynamic edges
|
|
this.fromBackup.detachEdge(this);
|
|
}
|
|
else if (this.toId != this.toBackup.id) { // to was changed, remove edge from old 'to' node dynamic edges
|
|
this.toBackup.detachEdge(this);
|
|
}
|
|
|
|
this.fromBackup = undefined;
|
|
this.toBackup = undefined;
|
|
this.controlNodesEnabled = false;
|
|
}
|
|
|
|
|
|
/**
|
|
* This checks if one of the control nodes is selected and if so, returns the control node object. Else it returns undefined.
|
|
* @param x
|
|
* @param y
|
|
* @returns {undefined}
|
|
* @private
|
|
*/
|
|
_getSelectedControlNode(x, y) {
|
|
var positions = this.controlNodes.positions;
|
|
var fromDistance = Math.sqrt(Math.pow(x - positions.from.x, 2) + Math.pow(y - positions.from.y, 2));
|
|
var toDistance = Math.sqrt(Math.pow(x - positions.to.x, 2) + Math.pow(y - positions.to.y, 2));
|
|
|
|
if (fromDistance < 15) {
|
|
this.connectedNode = this.from;
|
|
this.from = this.controlNodes.from;
|
|
return this.controlNodes.from;
|
|
}
|
|
else if (toDistance < 15) {
|
|
this.connectedNode = this.to;
|
|
this.to = this.controlNodes.to;
|
|
return this.controlNodes.to;
|
|
}
|
|
else {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* this resets the control nodes to their original position.
|
|
* @private
|
|
*/
|
|
_restoreControlNodes() {
|
|
if (this.controlNodes.from.selected == true) {
|
|
this.from = this.connectedNode;
|
|
this.connectedNode = undefined;
|
|
this.controlNodes.from.unselect();
|
|
}
|
|
else if (this.controlNodes.to.selected == true) {
|
|
this.to = this.connectedNode;
|
|
this.connectedNode = undefined;
|
|
this.controlNodes.to.unselect();
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* this calculates the position of the control nodes on the edges of the parent nodes.
|
|
*
|
|
* @param ctx
|
|
* @returns {x: *, y: *}
|
|
*/
|
|
getControlNodeFromPosition(ctx) {
|
|
// draw arrow head
|
|
var controlnodeFromPos;
|
|
if (this.options.smooth.enabled == true) {
|
|
controlnodeFromPos = this._findBorderPositionBezier(true, ctx);
|
|
}
|
|
else {
|
|
var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
|
|
var dx = (this.to.x - this.from.x);
|
|
var dy = (this.to.y - this.from.y);
|
|
var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
|
|
|
|
var fromBorderDist = this.from.distanceToBorder(ctx, angle + Math.PI);
|
|
var fromBorderPoint = (edgeSegmentLength - fromBorderDist) / edgeSegmentLength;
|
|
controlnodeFromPos = {};
|
|
controlnodeFromPos.x = (fromBorderPoint) * this.from.x + (1 - fromBorderPoint) * this.to.x;
|
|
controlnodeFromPos.y = (fromBorderPoint) * this.from.y + (1 - fromBorderPoint) * this.to.y;
|
|
}
|
|
|
|
return controlnodeFromPos;
|
|
}
|
|
|
|
|
|
/**
|
|
* this calculates the position of the control nodes on the edges of the parent nodes.
|
|
*
|
|
* @param ctx
|
|
* @returns {{from: {x: number, y: number}, to: {x: *, y: *}}}
|
|
*/
|
|
getControlNodeToPosition(ctx) {
|
|
// draw arrow head
|
|
var controlnodeToPos;
|
|
if (this.options.smooth.enabled == true) {
|
|
controlnodeToPos = this._findBorderPositionBezier(false, ctx);
|
|
}
|
|
else {
|
|
var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
|
|
var dx = (this.to.x - this.from.x);
|
|
var dy = (this.to.y - this.from.y);
|
|
var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
|
|
var toBorderDist = this.to.distanceToBorder(ctx, angle);
|
|
var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength;
|
|
|
|
controlnodeToPos = {};
|
|
controlnodeToPos.x = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x;
|
|
controlnodeToPos.y = (1 - toBorderPoint) * this.from.y + toBorderPoint * this.to.y;
|
|
}
|
|
|
|
return controlnodeToPos;
|
|
}
|
|
}
|
|
|
|
export default Edge;
|