var util = require('../../../../util');
|
|
|
|
/**
|
|
* @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.initializing = true;
|
|
|
|
this.options = util.bridgeObject(globalOptions);
|
|
this.body = body;
|
|
|
|
// initialize variables
|
|
this.id = undefined;
|
|
this.fromId = undefined;
|
|
this.toId = undefined;
|
|
this.title = undefined;
|
|
this.widthSelected = this.options.width * this.options.widthSelectionMultiplier;
|
|
this.value = undefined;
|
|
this.selected = false;
|
|
this.hover = false;
|
|
this.labelDimensions = {top: 0, left: 0, width: 0, height: 0, yLine: 0}; // could be cached
|
|
this.labelDirty = true;
|
|
this.colorDirty = true;
|
|
|
|
this.from = undefined; // a node
|
|
this.to = undefined; // a node
|
|
this.via = undefined; // a temp node
|
|
|
|
this.fromBackup = undefined; // used to clean up after reconnect (used for manipulation)
|
|
this.toBackup = undefined; // used to clean up after reconnect (used for manipulation)
|
|
|
|
// we use this to be able to reconnect the edge to a cluster if its node is put into a cluster
|
|
// by storing the original information we can revert to the original connection when the cluser is opened.
|
|
this.fromArray = [];
|
|
this.toArray = [];
|
|
|
|
this.connected = false;
|
|
|
|
this.widthFixed = false;
|
|
this.lengthFixed = false;
|
|
|
|
this.setOptions(options);
|
|
|
|
this.controlNodesEnabled = false;
|
|
this.controlNodes = {from: undefined, to: undefined, positions: {}};
|
|
this.connectedNode = undefined;
|
|
|
|
this.initializing = false;
|
|
}
|
|
|
|
|
|
/**
|
|
* Set or overwrite options for the edge
|
|
* @param {Object} options an object with options
|
|
* @param {Object} constants and object with default, global options
|
|
*/
|
|
setOptions(options) {
|
|
if (!options) {
|
|
return;
|
|
}
|
|
this.colorDirty = true;
|
|
|
|
var fields = ['style', 'fontSize', 'fontFace', 'fontColor', 'fontFill', 'fontStrokeWidth', 'fontStrokeColor', 'width',
|
|
'widthSelectionMultiplier', 'hoverWidth', 'arrowScaleFactor', 'dash', 'inheritColor', 'labelAlignment', 'opacity',
|
|
'customScalingFunction', 'useGradients', 'value','smooth','hidden','physics'
|
|
];
|
|
util.selectiveDeepExtend(fields, this.options, options);
|
|
|
|
if (options.from !== undefined) {
|
|
this.fromId = options.from;
|
|
}
|
|
if (options.to !== undefined) {
|
|
this.toId = options.to;
|
|
}
|
|
|
|
if (options.id !== undefined) {
|
|
this.id = options.id;
|
|
}
|
|
if (options.label !== undefined) {
|
|
this.label = options.label;
|
|
this.labelDirty = true;
|
|
}
|
|
|
|
if (options.title !== undefined) {
|
|
this.title = options.title;
|
|
}
|
|
if (options.value !== undefined) {
|
|
this.value = options.value;
|
|
}
|
|
if (options.length !== undefined) {
|
|
this.physics.springLength = options.length;
|
|
}
|
|
|
|
if (options.color !== undefined) {
|
|
this.options.inheritColor = false;
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
util.mergeOptions(this.options, options, 'smooth');
|
|
|
|
// A node is connected when it has a from and to node that both exist in the network.body.nodes.
|
|
this.connect();
|
|
|
|
this.widthFixed = this.widthFixed || (options.width !== undefined);
|
|
this.lengthFixed = this.lengthFixed || (options.length !== undefined);
|
|
|
|
this.widthSelected = this.options.width * this.options.widthSelectionMultiplier;
|
|
|
|
this.setupSmoothEdges(this.initializing === false);
|
|
|
|
// set draw method based on style
|
|
switch (this.options.style) {
|
|
case 'line':
|
|
this.draw = this._drawLine;
|
|
break;
|
|
case 'arrow':
|
|
this.draw = this._drawArrow;
|
|
break;
|
|
case 'arrow-center':
|
|
this.draw = this._drawArrowCenter;
|
|
break;
|
|
case 'dash-line':
|
|
this.draw = this._drawDashLine;
|
|
break;
|
|
default:
|
|
this.draw = this._drawLine;
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Bezier curves require an anchor point to calculate the smooth flow. These points are nodes. These nodes are invisible but
|
|
* are used for the force calculation.
|
|
*
|
|
* @private
|
|
*/
|
|
setupSmoothEdges(emitChange = true) {
|
|
var changedData = false;
|
|
if (this.options.smooth.enabled == true && this.options.smooth.dynamic == true) {
|
|
if (this.via === undefined) {
|
|
changedData = true;
|
|
var nodeId = "edgeId:" + this.id;
|
|
var node = this.body.functions.createNode({
|
|
id: nodeId,
|
|
mass: 1,
|
|
shape: 'circle',
|
|
image: "",
|
|
physics:true,
|
|
hidden:true
|
|
});
|
|
this.body.nodes[nodeId] = node;
|
|
this.via = node;
|
|
this.via.parentEdgeId = this.id;
|
|
this.positionBezierNode();
|
|
}
|
|
}
|
|
else {
|
|
if (this.via !== undefined) {
|
|
delete this.body.nodes[this.via.id];
|
|
this.via = undefined;
|
|
changedData = true;
|
|
}
|
|
}
|
|
|
|
// node has been added or deleted
|
|
if (changedData === true && emitChange === true) {
|
|
this.body.emitter.emit("_dataChanged");
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
setValueRange(min, max, total) {
|
|
if (!this.widthFixed && this.value !== undefined) {
|
|
var scale = this.options.customScalingFunction(min, max, total, this.value);
|
|
var widthDiff = this.options.widthMax - this.options.widthMin;
|
|
this.options.width = this.options.widthMin + scale * widthDiff;
|
|
this.widthSelected = this.options.width * this.options.widthSelectionMultiplier;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* 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) {
|
|
throw "Method draw not initialized in edge";
|
|
}
|
|
|
|
|
|
/**
|
|
* 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._getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj);
|
|
|
|
return (dist < distMax);
|
|
}
|
|
else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
|
|
_getColor(ctx) {
|
|
var colorObj = this.options.color;
|
|
if (this.options.useGradients == true) {
|
|
var grd = ctx.createLinearGradient(this.from.x, this.from.y, this.to.x, this.to.y);
|
|
var fromColor, toColor;
|
|
fromColor = this.from.options.color.highlight.border;
|
|
toColor = this.to.options.color.highlight.border;
|
|
|
|
|
|
if (this.from.selected == false && this.to.selected == false) {
|
|
fromColor = util.overrideOpacity(this.from.options.color.border, this.options.opacity);
|
|
toColor = util.overrideOpacity(this.to.options.color.border, this.options.opacity);
|
|
}
|
|
else if (this.from.selected == true && this.to.selected == false) {
|
|
toColor = this.to.options.color.border;
|
|
}
|
|
else if (this.from.selected == false && this.to.selected == true) {
|
|
fromColor = this.from.options.color.border;
|
|
}
|
|
grd.addColorStop(0, fromColor);
|
|
grd.addColorStop(1, toColor);
|
|
return grd;
|
|
}
|
|
|
|
if (this.colorDirty === true) {
|
|
|
|
if (this.options.inheritColor == "to") {
|
|
colorObj = {
|
|
highlight: this.to.options.color.highlight.border,
|
|
hover: this.to.options.color.hover.border,
|
|
color: util.overrideOpacity(this.from.options.color.border, this.options.opacity)
|
|
};
|
|
}
|
|
else if (this.options.inheritColor == "from" || this.options.inheritColor == true) {
|
|
colorObj = {
|
|
highlight: this.from.options.color.highlight.border,
|
|
hover: this.from.options.color.hover.border,
|
|
color: util.overrideOpacity(this.from.options.color.border, this.options.opacity)
|
|
};
|
|
}
|
|
this.options.color = colorObj;
|
|
this.colorDirty = false;
|
|
}
|
|
|
|
|
|
if (this.selected == true) {
|
|
return colorObj.highlight;
|
|
}
|
|
else if (this.hover == true) {
|
|
return colorObj.hover;
|
|
}
|
|
else {
|
|
return colorObj.color;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Redraw a edge as a line
|
|
* Draw this edge in the given canvas
|
|
* The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
|
|
* @param {CanvasRenderingContext2D} ctx
|
|
* @private
|
|
*/
|
|
_drawLine(ctx) {
|
|
// set style
|
|
ctx.strokeStyle = this._getColor(ctx);
|
|
ctx.lineWidth = this._getLineWidth();
|
|
|
|
if (this.from != this.to) {
|
|
// draw line
|
|
var via = this._line(ctx);
|
|
|
|
// draw label
|
|
var point;
|
|
if (this.label) {
|
|
if (this.options.smooth.enabled == true && via != undefined) {
|
|
var midpointX = 0.5 * (0.5 * (this.from.x + via.x) + 0.5 * (this.to.x + via.x));
|
|
var midpointY = 0.5 * (0.5 * (this.from.y + via.y) + 0.5 * (this.to.y + via.y));
|
|
point = {x: midpointX, y: midpointY};
|
|
}
|
|
else {
|
|
point = this._pointOnLine(0.5);
|
|
}
|
|
this._label(ctx, this.label, point.x, point.y);
|
|
}
|
|
}
|
|
else {
|
|
var x, y;
|
|
var radius = this.physics.springLength / 4;
|
|
var node = this.from;
|
|
if (!node.width) {
|
|
node.resize(ctx);
|
|
}
|
|
if (node.width > node.height) {
|
|
x = node.x + node.width / 2;
|
|
y = node.y - radius;
|
|
}
|
|
else {
|
|
x = node.x + radius;
|
|
y = node.y - node.height / 2;
|
|
}
|
|
this._circle(ctx, x, y, radius);
|
|
point = this._pointOnCircle(x, y, radius, 0.5);
|
|
this._label(ctx, this.label, point.x, point.y);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the line width of the edge. Depends on width and whether one of the
|
|
* connected nodes is selected.
|
|
* @return {Number} width
|
|
* @private
|
|
*/
|
|
_getLineWidth() {
|
|
if (this.selected == true) {
|
|
return Math.max(Math.min(this.widthSelected, this.options.widthMax), 0.3 * this.networkScaleInv);
|
|
}
|
|
else {
|
|
if (this.hover == true) {
|
|
return Math.max(Math.min(this.options.hoverWidth, this.options.widthMax), 0.3 * this.networkScaleInv);
|
|
}
|
|
else {
|
|
return Math.max(this.options.width, 0.3 * this.networkScaleInv);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
_getViaCoordinates() {
|
|
if (this.options.smooth.dynamic == true && this.options.smooth.enabled == true) {
|
|
return this.via;
|
|
}
|
|
else if (this.options.smooth.enabled == false) {
|
|
return {x: 0, y: 0};
|
|
}
|
|
else {
|
|
let xVia = undefined;
|
|
let yVia = undefined;
|
|
let factor = this.options.smooth.roundness;
|
|
let type = this.options.smooth.type;
|
|
let dx = Math.abs(this.from.x - this.to.x);
|
|
let dy = Math.abs(this.from.y - this.to.y);
|
|
if (type == 'discrete' || type == 'diagonalCross') {
|
|
if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) {
|
|
if (this.from.y > this.to.y) {
|
|
if (this.from.x < this.to.x) {
|
|
xVia = this.from.x + factor * dy;
|
|
yVia = this.from.y - factor * dy;
|
|
}
|
|
else if (this.from.x > this.to.x) {
|
|
xVia = this.from.x - factor * dy;
|
|
yVia = this.from.y - factor * dy;
|
|
}
|
|
}
|
|
else if (this.from.y < this.to.y) {
|
|
if (this.from.x < this.to.x) {
|
|
xVia = this.from.x + factor * dy;
|
|
yVia = this.from.y + factor * dy;
|
|
}
|
|
else if (this.from.x > this.to.x) {
|
|
xVia = this.from.x - factor * dy;
|
|
yVia = this.from.y + factor * dy;
|
|
}
|
|
}
|
|
if (type == "discrete") {
|
|
xVia = dx < factor * dy ? this.from.x : xVia;
|
|
}
|
|
}
|
|
else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) {
|
|
if (this.from.y > this.to.y) {
|
|
if (this.from.x < this.to.x) {
|
|
xVia = this.from.x + factor * dx;
|
|
yVia = this.from.y - factor * dx;
|
|
}
|
|
else if (this.from.x > this.to.x) {
|
|
xVia = this.from.x - factor * dx;
|
|
yVia = this.from.y - factor * dx;
|
|
}
|
|
}
|
|
else if (this.from.y < this.to.y) {
|
|
if (this.from.x < this.to.x) {
|
|
xVia = this.from.x + factor * dx;
|
|
yVia = this.from.y + factor * dx;
|
|
}
|
|
else if (this.from.x > this.to.x) {
|
|
xVia = this.from.x - factor * dx;
|
|
yVia = this.from.y + factor * dx;
|
|
}
|
|
}
|
|
if (type == "discrete") {
|
|
yVia = dy < factor * dx ? this.from.y : yVia;
|
|
}
|
|
}
|
|
}
|
|
else if (type == "straightCross") {
|
|
if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) { // up - down
|
|
xVia = this.from.x;
|
|
if (this.from.y < this.to.y) {
|
|
yVia = this.to.y - (1 - factor) * dy;
|
|
}
|
|
else {
|
|
yVia = this.to.y + (1 - factor) * dy;
|
|
}
|
|
}
|
|
else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) { // left - right
|
|
if (this.from.x < this.to.x) {
|
|
xVia = this.to.x - (1 - factor) * dx;
|
|
}
|
|
else {
|
|
xVia = this.to.x + (1 - factor) * dx;
|
|
}
|
|
yVia = this.from.y;
|
|
}
|
|
}
|
|
else if (type == 'horizontal') {
|
|
if (this.from.x < this.to.x) {
|
|
xVia = this.to.x - (1 - factor) * dx;
|
|
}
|
|
else {
|
|
xVia = this.to.x + (1 - factor) * dx;
|
|
}
|
|
yVia = this.from.y;
|
|
}
|
|
else if (type == 'vertical') {
|
|
xVia = this.from.x;
|
|
if (this.from.y < this.to.y) {
|
|
yVia = this.to.y - (1 - factor) * dy;
|
|
}
|
|
else {
|
|
yVia = this.to.y + (1 - factor) * dy;
|
|
}
|
|
}
|
|
else if (type == 'curvedCW') {
|
|
dx = this.to.x - this.from.x;
|
|
dy = this.from.y - this.to.y;
|
|
let radius = Math.sqrt(dx * dx + dy * dy);
|
|
let pi = Math.PI;
|
|
|
|
let originalAngle = Math.atan2(dy, dx);
|
|
let myAngle = (originalAngle + ((factor * 0.5) + 0.5) * pi) % (2 * pi);
|
|
|
|
xVia = this.from.x + (factor * 0.5 + 0.5) * radius * Math.sin(myAngle);
|
|
yVia = this.from.y + (factor * 0.5 + 0.5) * radius * Math.cos(myAngle);
|
|
}
|
|
else if (type == 'curvedCCW') {
|
|
dx = this.to.x - this.from.x;
|
|
dy = this.from.y - this.to.y;
|
|
let radius = Math.sqrt(dx * dx + dy * dy);
|
|
let pi = Math.PI;
|
|
|
|
let originalAngle = Math.atan2(dy, dx);
|
|
let myAngle = (originalAngle + ((-factor * 0.5) + 0.5) * pi) % (2 * pi);
|
|
|
|
xVia = this.from.x + (factor * 0.5 + 0.5) * radius * Math.sin(myAngle);
|
|
yVia = this.from.y + (factor * 0.5 + 0.5) * radius * Math.cos(myAngle);
|
|
}
|
|
else { // continuous
|
|
if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) {
|
|
if (this.from.y > this.to.y) {
|
|
if (this.from.x < this.to.x) {
|
|
xVia = this.from.x + factor * dy;
|
|
yVia = this.from.y - factor * dy;
|
|
xVia = this.to.x < xVia ? this.to.x : xVia;
|
|
}
|
|
else if (this.from.x > this.to.x) {
|
|
xVia = this.from.x - factor * dy;
|
|
yVia = this.from.y - factor * dy;
|
|
xVia = this.to.x > xVia ? this.to.x : xVia;
|
|
}
|
|
}
|
|
else if (this.from.y < this.to.y) {
|
|
if (this.from.x < this.to.x) {
|
|
xVia = this.from.x + factor * dy;
|
|
yVia = this.from.y + factor * dy;
|
|
xVia = this.to.x < xVia ? this.to.x : xVia;
|
|
}
|
|
else if (this.from.x > this.to.x) {
|
|
xVia = this.from.x - factor * dy;
|
|
yVia = this.from.y + factor * dy;
|
|
xVia = this.to.x > xVia ? this.to.x : xVia;
|
|
}
|
|
}
|
|
}
|
|
else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) {
|
|
if (this.from.y > this.to.y) {
|
|
if (this.from.x < this.to.x) {
|
|
xVia = this.from.x + factor * dx;
|
|
yVia = this.from.y - factor * dx;
|
|
yVia = this.to.y > yVia ? this.to.y : yVia;
|
|
}
|
|
else if (this.from.x > this.to.x) {
|
|
xVia = this.from.x - factor * dx;
|
|
yVia = this.from.y - factor * dx;
|
|
yVia = this.to.y > yVia ? this.to.y : yVia;
|
|
}
|
|
}
|
|
else if (this.from.y < this.to.y) {
|
|
if (this.from.x < this.to.x) {
|
|
xVia = this.from.x + factor * dx;
|
|
yVia = this.from.y + factor * dx;
|
|
yVia = this.to.y < yVia ? this.to.y : yVia;
|
|
}
|
|
else if (this.from.x > this.to.x) {
|
|
xVia = this.from.x - factor * dx;
|
|
yVia = this.from.y + factor * dx;
|
|
yVia = this.to.y < yVia ? this.to.y : yVia;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
return {x: xVia, y: yVia};
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Draw a line between two nodes
|
|
* @param {CanvasRenderingContext2D} ctx
|
|
* @private
|
|
*/
|
|
_line(ctx) {
|
|
// draw a straight line
|
|
ctx.beginPath();
|
|
ctx.moveTo(this.from.x, this.from.y);
|
|
if (this.options.smooth.enabled == true) {
|
|
if (this.options.smooth.dynamic == false) {
|
|
var via = this._getViaCoordinates();
|
|
if (via.x === undefined) {
|
|
ctx.lineTo(this.to.x, this.to.y);
|
|
ctx.stroke();
|
|
return undefined;
|
|
}
|
|
else {
|
|
// this.via.x = via.x;
|
|
// this.via.y = via.y;
|
|
ctx.quadraticCurveTo(via.x, via.y, this.to.x, this.to.y);
|
|
ctx.stroke();
|
|
//ctx.circle(via.x,via.y,2)
|
|
//ctx.stroke();
|
|
return via;
|
|
}
|
|
}
|
|
else {
|
|
ctx.quadraticCurveTo(this.via.x, this.via.y, this.to.x, this.to.y);
|
|
ctx.stroke();
|
|
return this.via;
|
|
}
|
|
}
|
|
else {
|
|
ctx.lineTo(this.to.x, this.to.y);
|
|
ctx.stroke();
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Draw a line from a node to itself, a circle
|
|
* @param {CanvasRenderingContext2D} ctx
|
|
* @param {Number} x
|
|
* @param {Number} y
|
|
* @param {Number} radius
|
|
* @private
|
|
*/
|
|
_circle(ctx, x, y, radius) {
|
|
// draw a circle
|
|
ctx.beginPath();
|
|
ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
|
|
ctx.stroke();
|
|
}
|
|
|
|
|
|
/**
|
|
* Draw label with white background and with the middle at (x, y)
|
|
* @param {CanvasRenderingContext2D} ctx
|
|
* @param {String} text
|
|
* @param {Number} x
|
|
* @param {Number} y
|
|
* @private
|
|
*/
|
|
_label(ctx, text, x, y) {
|
|
if (text) {
|
|
ctx.font = ((this.from.selected || this.to.selected) ? "bold " : "") +
|
|
this.options.fontSize + "px " + this.options.fontFace;
|
|
var yLine;
|
|
|
|
if (this.labelDirty == true) {
|
|
var lines = String(text).split('\n');
|
|
var lineCount = lines.length;
|
|
var fontSize = Number(this.options.fontSize);
|
|
yLine = y + (1 - lineCount) / 2 * fontSize;
|
|
|
|
var width = ctx.measureText(lines[0]).width;
|
|
for (var i = 1; i < lineCount; i++) {
|
|
var lineWidth = ctx.measureText(lines[i]).width;
|
|
width = lineWidth > width ? lineWidth : width;
|
|
}
|
|
var height = this.options.fontSize * lineCount;
|
|
var left = x - width / 2;
|
|
var top = y - height / 2;
|
|
|
|
// cache
|
|
this.labelDimensions = {top: top, left: left, width: width, height: height, yLine: yLine};
|
|
}
|
|
|
|
var yLine = this.labelDimensions.yLine;
|
|
|
|
ctx.save();
|
|
|
|
if (this.options.labelAlignment != "horizontal") {
|
|
ctx.translate(x, yLine);
|
|
this._rotateForLabelAlignment(ctx);
|
|
x = 0;
|
|
yLine = 0;
|
|
}
|
|
|
|
|
|
this._drawLabelRect(ctx);
|
|
this._drawLabelText(ctx, x, yLine, lines, lineCount, fontSize);
|
|
|
|
ctx.restore();
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
|
|
|
|
/**
|
|
* Draws the label rectangle
|
|
* @param {CanvasRenderingContext2D} ctx
|
|
* @param {String} labelAlignment
|
|
* @private
|
|
*/
|
|
_drawLabelRect(ctx) {
|
|
if (this.options.fontFill !== undefined && this.options.fontFill !== undefined && this.options.fontFill !== "none") {
|
|
ctx.fillStyle = this.options.fontFill;
|
|
|
|
var lineMargin = 2;
|
|
|
|
if (this.options.labelAlignment == 'line-center') {
|
|
ctx.fillRect(-this.labelDimensions.width * 0.5, -this.labelDimensions.height * 0.5, this.labelDimensions.width, this.labelDimensions.height);
|
|
}
|
|
else if (this.options.labelAlignment == 'line-above') {
|
|
ctx.fillRect(-this.labelDimensions.width * 0.5, -(this.labelDimensions.height + lineMargin), this.labelDimensions.width, this.labelDimensions.height);
|
|
}
|
|
else if (this.options.labelAlignment == 'line-below') {
|
|
ctx.fillRect(-this.labelDimensions.width * 0.5, lineMargin, this.labelDimensions.width, this.labelDimensions.height);
|
|
}
|
|
else {
|
|
ctx.fillRect(this.labelDimensions.left, this.labelDimensions.top, this.labelDimensions.width, this.labelDimensions.height);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Draws the label text
|
|
* @param {CanvasRenderingContext2D} ctx
|
|
* @param {Number} x
|
|
* @param {Number} yLine
|
|
* @param {Array} lines
|
|
* @param {Number} lineCount
|
|
* @param {Number} fontSize
|
|
* @private
|
|
*/
|
|
_drawLabelText(ctx, x, yLine, lines, lineCount, fontSize) {
|
|
// draw text
|
|
ctx.fillStyle = this.options.fontColor || "black";
|
|
ctx.textAlign = "center";
|
|
|
|
// check for label alignment
|
|
if (this.options.labelAlignment != 'horizontal') {
|
|
var lineMargin = 2;
|
|
if (this.options.labelAlignment == 'line-above') {
|
|
ctx.textBaseline = "alphabetic";
|
|
yLine -= 2 * lineMargin; // distance from edge, required because we use alphabetic. Alphabetic has less difference between browsers
|
|
}
|
|
else if (this.options.labelAlignment == 'line-below') {
|
|
ctx.textBaseline = "hanging";
|
|
yLine += 2 * lineMargin;// distance from edge, required because we use hanging. Hanging has less difference between browsers
|
|
}
|
|
else {
|
|
ctx.textBaseline = "middle";
|
|
}
|
|
}
|
|
else {
|
|
ctx.textBaseline = "middle";
|
|
}
|
|
|
|
// check for strokeWidth
|
|
if (this.options.fontStrokeWidth > 0) {
|
|
ctx.lineWidth = this.options.fontStrokeWidth;
|
|
ctx.strokeStyle = this.options.fontStrokeColor;
|
|
ctx.lineJoin = 'round';
|
|
}
|
|
for (var i = 0; i < lineCount; i++) {
|
|
if (this.options.fontStrokeWidth > 0) {
|
|
ctx.strokeText(lines[i], x, yLine);
|
|
}
|
|
ctx.fillText(lines[i], x, yLine);
|
|
yLine += fontSize;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Redraw a edge as a dashed line
|
|
* Draw this edge in the given canvas
|
|
* @author David Jordan
|
|
* @date 2012-08-08
|
|
* The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
|
|
* @param {CanvasRenderingContext2D} ctx
|
|
* @private
|
|
*/
|
|
_drawDashLine(ctx) {
|
|
// set style
|
|
ctx.strokeStyle = this._getColor(ctx);
|
|
ctx.lineWidth = this._getLineWidth();
|
|
|
|
var via = undefined;
|
|
// only firefox and chrome support this method, else we use the legacy one.
|
|
if (ctx.setLineDash !== undefined) {
|
|
ctx.save();
|
|
// configure the dash pattern
|
|
var pattern = [0];
|
|
if (this.options.dash.length !== undefined && this.options.dash.gap !== undefined) {
|
|
pattern = [this.options.dash.length, this.options.dash.gap];
|
|
}
|
|
else {
|
|
pattern = [5, 5];
|
|
}
|
|
|
|
// set dash settings for chrome or firefox
|
|
ctx.setLineDash(pattern);
|
|
ctx.lineDashOffset = 0;
|
|
|
|
// draw the line
|
|
via = this._line(ctx);
|
|
|
|
// restore the dash settings.
|
|
ctx.setLineDash([0]);
|
|
ctx.lineDashOffset = 0;
|
|
ctx.restore();
|
|
}
|
|
else { // unsupporting smooth lines
|
|
// draw dashed line
|
|
ctx.beginPath();
|
|
ctx.lineCap = 'round';
|
|
if (this.options.dash.altLength !== undefined) //If an alt dash value has been set add to the array this value
|
|
{
|
|
ctx.dashedLine(this.from.x, this.from.y, this.to.x, this.to.y,
|
|
[this.options.dash.length, this.options.dash.gap, this.options.dash.altLength, this.options.dash.gap]);
|
|
}
|
|
else if (this.options.dash.length !== undefined && this.options.dash.gap !== undefined) //If a dash and gap value has been set add to the array this value
|
|
{
|
|
ctx.dashedLine(this.from.x, this.from.y, this.to.x, this.to.y,
|
|
[this.options.dash.length, this.options.dash.gap]);
|
|
}
|
|
else //If all else fails draw a line
|
|
{
|
|
ctx.moveTo(this.from.x, this.from.y);
|
|
ctx.lineTo(this.to.x, this.to.y);
|
|
}
|
|
ctx.stroke();
|
|
}
|
|
|
|
// draw label
|
|
if (this.label) {
|
|
var point;
|
|
if (this.options.smooth.enabled == true && via != undefined) {
|
|
var midpointX = 0.5 * (0.5 * (this.from.x + via.x) + 0.5 * (this.to.x + via.x));
|
|
var midpointY = 0.5 * (0.5 * (this.from.y + via.y) + 0.5 * (this.to.y + via.y));
|
|
point = {x: midpointX, y: midpointY};
|
|
}
|
|
else {
|
|
point = this._pointOnLine(0.5);
|
|
}
|
|
this._label(ctx, this.label, point.x, point.y);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Get a point on a line
|
|
* @param {Number} percentage. Value between 0 (line start) and 1 (line end)
|
|
* @return {Object} point
|
|
* @private
|
|
*/
|
|
_pointOnLine(percentage) {
|
|
return {
|
|
x: (1 - percentage) * this.from.x + percentage * this.to.x,
|
|
y: (1 - percentage) * this.from.y + percentage * this.to.y
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* 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 - 3 / 8) * 2 * Math.PI;
|
|
return {
|
|
x: x + radius * Math.cos(angle),
|
|
y: y - radius * Math.sin(angle)
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Redraw a edge as a line with an arrow halfway the line
|
|
* Draw this edge in the given canvas
|
|
* The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
|
|
* @param {CanvasRenderingContext2D} ctx
|
|
* @private
|
|
*/
|
|
_drawArrowCenter(ctx) {
|
|
var point;
|
|
// set style
|
|
ctx.strokeStyle = this._getColor(ctx);
|
|
ctx.fillStyle = ctx.strokeStyle;
|
|
ctx.lineWidth = this._getLineWidth();
|
|
|
|
if (this.from != this.to) {
|
|
// draw line
|
|
var via = this._line(ctx);
|
|
|
|
var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
|
|
var length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor;
|
|
// draw an arrow halfway the line
|
|
if (this.options.smooth.enabled == true && via != undefined) {
|
|
var midpointX = 0.5 * (0.5 * (this.from.x + via.x) + 0.5 * (this.to.x + via.x));
|
|
var midpointY = 0.5 * (0.5 * (this.from.y + via.y) + 0.5 * (this.to.y + via.y));
|
|
point = {x: midpointX, y: midpointY};
|
|
}
|
|
else {
|
|
point = this._pointOnLine(0.5);
|
|
}
|
|
|
|
ctx.arrow(point.x, point.y, angle, length);
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
|
|
// draw label
|
|
if (this.label) {
|
|
this._label(ctx, this.label, point.x, point.y);
|
|
}
|
|
}
|
|
else {
|
|
// draw circle
|
|
var x, y;
|
|
var radius = 0.25 * Math.max(100, this.physics.springLength);
|
|
var node = this.from;
|
|
if (!node.width) {
|
|
node.resize(ctx);
|
|
}
|
|
if (node.width > node.height) {
|
|
x = node.x + node.width * 0.5;
|
|
y = node.y - radius;
|
|
}
|
|
else {
|
|
x = node.x + radius;
|
|
y = node.y - node.height * 0.5;
|
|
}
|
|
this._circle(ctx, x, y, radius);
|
|
|
|
// draw all arrows
|
|
var angle = 0.2 * Math.PI;
|
|
var length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor;
|
|
point = this._pointOnCircle(x, y, radius, 0.5);
|
|
ctx.arrow(point.x, point.y, angle, length);
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
|
|
// draw label
|
|
if (this.label) {
|
|
point = this._pointOnCircle(x, y, radius, 0.5);
|
|
this._label(ctx, this.label, point.x, point.y);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
_pointOnBezier(t) {
|
|
var via = this._getViaCoordinates();
|
|
|
|
var x = Math.pow(1 - t, 2) * this.from.x + (2 * t * (1 - t)) * via.x + Math.pow(t, 2) * this.to.x;
|
|
var y = Math.pow(1 - t, 2) * this.from.y + (2 * t * (1 - t)) * via.y + Math.pow(t, 2) * this.to.y;
|
|
|
|
return {x: x, y: y};
|
|
}
|
|
|
|
/**
|
|
* This function uses binary search to look for the point where the bezier curve crosses the border of the node.
|
|
*
|
|
* @param from
|
|
* @param ctx
|
|
* @returns {*}
|
|
* @private
|
|
*/
|
|
_findBorderPosition(from, ctx) {
|
|
var maxIterations = 10;
|
|
var iteration = 0;
|
|
var low = 0;
|
|
var high = 1;
|
|
var pos, angle, distanceToBorder, distanceToNodes, difference;
|
|
var threshold = 0.2;
|
|
var node = this.to;
|
|
if (from == true) {
|
|
node = this.from;
|
|
}
|
|
|
|
while (low <= high && iteration < maxIterations) {
|
|
var middle = (low + high) * 0.5;
|
|
|
|
pos = this._pointOnBezier(middle);
|
|
angle = Math.atan2((node.y - pos.y), (node.x - pos.x));
|
|
distanceToBorder = node.distanceToBorder(ctx, angle);
|
|
distanceToNodes = Math.sqrt(Math.pow(pos.x - node.x, 2) + Math.pow(pos.y - node.y, 2));
|
|
difference = distanceToBorder - distanceToNodes;
|
|
if (Math.abs(difference) < threshold) {
|
|
break; // found
|
|
}
|
|
else if (difference < 0) { // distance to nodes is larger than distance to border --> t needs to be bigger if we're looking at the to node.
|
|
if (from == false) {
|
|
low = middle;
|
|
}
|
|
else {
|
|
high = middle;
|
|
}
|
|
}
|
|
else {
|
|
if (from == false) {
|
|
high = middle;
|
|
}
|
|
else {
|
|
low = middle;
|
|
}
|
|
}
|
|
|
|
iteration++;
|
|
}
|
|
pos.t = middle;
|
|
|
|
return pos;
|
|
}
|
|
|
|
|
|
/**
|
|
* Redraw a edge as a line with an arrow
|
|
* Draw this edge in the given canvas
|
|
* The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
|
|
* @param {CanvasRenderingContext2D} ctx
|
|
* @private
|
|
*/
|
|
_drawArrow(ctx) {
|
|
// set style
|
|
ctx.strokeStyle = this._getColor(ctx);
|
|
ctx.fillStyle = ctx.strokeStyle;
|
|
ctx.lineWidth = this._getLineWidth();
|
|
|
|
// set vars
|
|
var angle, length, arrowPos;
|
|
|
|
// if not connected to itself
|
|
if (this.from != this.to) {
|
|
// draw line
|
|
this._line(ctx);
|
|
|
|
// draw arrow head
|
|
if (this.options.smooth.enabled == true) {
|
|
var via = this._getViaCoordinates();
|
|
arrowPos = this._findBorderPosition(false, ctx);
|
|
var guidePos = this._pointOnBezier(Math.max(0.0, arrowPos.t - 0.1))
|
|
angle = Math.atan2((arrowPos.y - guidePos.y), (arrowPos.x - guidePos.x));
|
|
}
|
|
else {
|
|
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;
|
|
|
|
arrowPos = {};
|
|
arrowPos.x = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x;
|
|
arrowPos.y = (1 - toBorderPoint) * this.from.y + toBorderPoint * this.to.y;
|
|
}
|
|
|
|
// draw arrow at the end of the line
|
|
length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor;
|
|
ctx.arrow(arrowPos.x, arrowPos.y, angle, length);
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
|
|
// draw label
|
|
if (this.label) {
|
|
var point;
|
|
if (this.options.smooth.enabled == true && via != undefined) {
|
|
point = this._pointOnBezier(0.5);
|
|
}
|
|
else {
|
|
point = this._pointOnLine(0.5);
|
|
}
|
|
this._label(ctx, this.label, point.x, point.y);
|
|
}
|
|
}
|
|
else {
|
|
// draw circle
|
|
var node = this.from;
|
|
var x, y, arrow;
|
|
var radius = 0.25 * Math.max(100, this.physics.springLength);
|
|
if (!node.width) {
|
|
node.resize(ctx);
|
|
}
|
|
if (node.width > node.height) {
|
|
x = node.x + node.width * 0.5;
|
|
y = node.y - radius;
|
|
arrow = {
|
|
x: x,
|
|
y: node.y,
|
|
angle: 0.9 * Math.PI
|
|
};
|
|
}
|
|
else {
|
|
x = node.x + radius;
|
|
y = node.y - node.height * 0.5;
|
|
arrow = {
|
|
x: node.x,
|
|
y: y,
|
|
angle: 0.6 * Math.PI
|
|
};
|
|
}
|
|
ctx.beginPath();
|
|
// TODO: similarly, for a line without arrows, draw to the border of the nodes instead of the center
|
|
ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
|
|
ctx.stroke();
|
|
|
|
// draw all arrows
|
|
var length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor;
|
|
ctx.arrow(arrow.x, arrow.y, arrow.angle, length);
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
|
|
// draw label
|
|
if (this.label) {
|
|
point = this._pointOnCircle(x, y, radius, 0.5);
|
|
this._label(ctx, this.label, point.x, point.y);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Calculate the distance between a point (x3,y3) and a line segment from
|
|
* (x1,y1) to (x2,y2).
|
|
* http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment
|
|
* @param {number} x1
|
|
* @param {number} y1
|
|
* @param {number} x2
|
|
* @param {number} y2
|
|
* @param {number} x3
|
|
* @param {number} y3
|
|
* @private
|
|
*/
|
|
_getDistanceToEdge(x1, y1, x2, y2, x3, y3) { // x3,y3 is the point
|
|
var returnValue = 0;
|
|
if (this.from != this.to) {
|
|
if (this.options.smooth.enabled == true) {
|
|
var xVia, yVia;
|
|
if (this.options.smooth.enabled == true && this.options.smooth.dynamic == true) {
|
|
xVia = this.via.x;
|
|
yVia = this.via.y;
|
|
}
|
|
else {
|
|
var via = this._getViaCoordinates();
|
|
xVia = via.x;
|
|
yVia = via.y;
|
|
}
|
|
var minDistance = 1e9;
|
|
var distance;
|
|
var i, t, x, y, lastX, lastY;
|
|
for (i = 0; i < 10; i++) {
|
|
t = 0.1 * i;
|
|
x = Math.pow(1 - t, 2) * x1 + (2 * t * (1 - t)) * xVia + Math.pow(t, 2) * x2;
|
|
y = Math.pow(1 - t, 2) * y1 + (2 * t * (1 - t)) * yVia + Math.pow(t, 2) * y2;
|
|
if (i > 0) {
|
|
distance = this._getDistanceToLine(lastX, lastY, x, y, x3, y3);
|
|
minDistance = distance < minDistance ? distance : minDistance;
|
|
}
|
|
lastX = x;
|
|
lastY = y;
|
|
}
|
|
returnValue = minDistance;
|
|
}
|
|
else {
|
|
returnValue = this._getDistanceToLine(x1, y1, x2, y2, x3, y3);
|
|
}
|
|
}
|
|
else {
|
|
var x, y, dx, dy;
|
|
var radius = 0.25 * this.physics.springLength;
|
|
var node = this.from;
|
|
if (node.width > node.height) {
|
|
x = node.x + 0.5 * node.width;
|
|
y = node.y - radius;
|
|
}
|
|
else {
|
|
x = node.x + radius;
|
|
y = node.y - 0.5 * node.height;
|
|
}
|
|
dx = x - x3;
|
|
dy = y - y3;
|
|
returnValue = Math.abs(Math.sqrt(dx * dx + dy * dy) - radius);
|
|
}
|
|
|
|
if (this.labelDimensions.left < x3 &&
|
|
this.labelDimensions.left + this.labelDimensions.width > x3 &&
|
|
this.labelDimensions.top < y3 &&
|
|
this.labelDimensions.top + this.labelDimensions.height > y3) {
|
|
return 0;
|
|
}
|
|
else {
|
|
return returnValue;
|
|
}
|
|
}
|
|
|
|
_getDistanceToLine(x1, y1, x2, y2, x3, y3) {
|
|
var px = x2 - x1,
|
|
py = y2 - y1,
|
|
something = px * px + py * py,
|
|
u = ((x3 - x1) * px + (y3 - y1) * py) / something;
|
|
|
|
if (u > 1) {
|
|
u = 1;
|
|
}
|
|
else if (u < 0) {
|
|
u = 0;
|
|
}
|
|
|
|
var x = x1 + u * px,
|
|
y = y1 + u * py,
|
|
dx = x - x3,
|
|
dy = y - y3;
|
|
|
|
//# Note: If the actual distance does not matter,
|
|
//# if you only want to compare what this function
|
|
//# returns to other results of this function, you
|
|
//# can just return the squared distance instead
|
|
//# (i.e. remove the sqrt) to gain a little performance
|
|
|
|
return Math.sqrt(dx * dx + dy * dy);
|
|
}
|
|
|
|
|
|
/**
|
|
* This allows the zoom level of the network to influence the rendering
|
|
*
|
|
* @param scale
|
|
*/
|
|
setScale(scale) {
|
|
this.networkScaleInv = 1.0 / scale;
|
|
}
|
|
|
|
|
|
|
|
select() {
|
|
this.selected = true;
|
|
}
|
|
|
|
|
|
unselect() {
|
|
this.selected = false;
|
|
}
|
|
|
|
positionBezierNode() {
|
|
if (this.via !== undefined && this.from !== undefined && this.to !== undefined) {
|
|
this.via.x = 0.5 * (this.from.x + this.to.x);
|
|
this.via.y = 0.5 * (this.from.y + this.to.y);
|
|
}
|
|
else if (this.via !== undefined) {
|
|
this.via.x = 0;
|
|
this.via.y = 0;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* 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._findBorderPosition(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._findBorderPosition(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;
|