|
|
@ -2,6 +2,9 @@ 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 |
|
|
|
* |
|
|
@ -31,7 +34,6 @@ class Edge { |
|
|
|
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; |
|
|
@ -40,15 +42,8 @@ class Edge { |
|
|
|
|
|
|
|
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.edgeType = undefined; |
|
|
|
|
|
|
|
this.connected = false; |
|
|
|
|
|
|
@ -76,6 +71,7 @@ class Edge { |
|
|
|
var fields = [ |
|
|
|
'id', |
|
|
|
'font', |
|
|
|
'from', |
|
|
|
'hidden', |
|
|
|
'hoverWidth', |
|
|
|
'label', |
|
|
@ -85,6 +81,7 @@ class Edge { |
|
|
|
'physics', |
|
|
|
'scaling', |
|
|
|
'selfReferenceSize', |
|
|
|
'to', |
|
|
|
'value', |
|
|
|
'width', |
|
|
|
'widthMin', |
|
|
@ -137,55 +134,48 @@ class Edge { |
|
|
|
// A node is connected when it has a from and to node that both exist in the network.body.nodes.
|
|
|
|
this.connect(); |
|
|
|
|
|
|
|
this.widthSelected = this.options.width * this.options.widthSelectionMultiplier; |
|
|
|
this.labelModule.setOptions(this.options); |
|
|
|
|
|
|
|
this.setupSmoothEdges(doNotEmit); |
|
|
|
this.updateEdgeType(); |
|
|
|
|
|
|
|
this.edgeType.setOptions(this.options); |
|
|
|
|
|
|
|
this.labelModule.setOptions(this.options); |
|
|
|
} |
|
|
|
|
|
|
|
updateEdgeType() { |
|
|
|
if (this.edgeType !== undefined) { |
|
|
|
this.edgeType.cleanup(); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 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(doNotEmit = false) { |
|
|
|
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(); |
|
|
|
if (this.options.smooth.enabled === true) { |
|
|
|
if (this.options.smooth.dynamic === true) { |
|
|
|
this.edgeType = new BezierEdgeDynamic(this.options, this.body, this.labelModule); |
|
|
|
} |
|
|
|
else { |
|
|
|
this.edgeType = new BezierEdgeStatic(this.options, this.body, this.labelModule); |
|
|
|
} |
|
|
|
} |
|
|
|
else { |
|
|
|
if (this.via !== undefined) { |
|
|
|
delete this.body.nodes[this.via.id]; |
|
|
|
this.via = undefined; |
|
|
|
changedData = true; |
|
|
|
} |
|
|
|
this.edgeType = new StraightEdge(this.options, this.body, this.labelModule); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// node has been added or deleted
|
|
|
|
if (changedData === true && doNotEmit === false) { |
|
|
|
this.body.emitter.emit("_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 |
|
|
|
*/ |
|
|
@ -284,26 +274,15 @@ class Edge { |
|
|
|
* @param {CanvasRenderingContext2D} ctx |
|
|
|
*/ |
|
|
|
draw(ctx) { |
|
|
|
let via = this.drawLine(ctx); |
|
|
|
let via = this.edgeType.drawLine(ctx, this.selected, this.hover); |
|
|
|
this.drawArrows(ctx, via); |
|
|
|
this.drawLabel (ctx, via); |
|
|
|
} |
|
|
|
|
|
|
|
drawLine(ctx) { |
|
|
|
if (this.options.dashes.enabled === false) { |
|
|
|
return this._drawLine(ctx); |
|
|
|
} |
|
|
|
else { |
|
|
|
return this._drawDashLine(ctx); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
drawArrows(ctx, viaNode) { |
|
|
|
if (this.options.arrows.from.enabled === true) {this._drawArrowHead(ctx,'from', viaNode);} |
|
|
|
if (this.options.arrows.middle.enabled === true) {this._drawArrowHead(ctx,'middle', viaNode);} |
|
|
|
if (this.options.arrows.to.enabled === true) {this._drawArrowHead(ctx,'to', viaNode);} |
|
|
|
|
|
|
|
|
|
|
|
if (this.options.arrows.from.enabled === true) {this._drawArrowHead(ctx,'from', viaNode);} |
|
|
|
if (this.options.arrows.middle.enabled === true) {this._drawArrowHead(ctx,'middle', viaNode);} |
|
|
|
if (this.options.arrows.to.enabled === true) {this._drawArrowHead(ctx,'to', viaNode);} |
|
|
|
} |
|
|
|
|
|
|
|
drawLabel(ctx, viaNode) { |
|
|
@ -313,7 +292,7 @@ class Edge { |
|
|
|
var node2 = this.to; |
|
|
|
var selected = (this.from.selected || this.to.selected || this.selected); |
|
|
|
if (node1.id != node2.id) { |
|
|
|
var point = this._pointOnEdge(0.5, viaNode); |
|
|
|
var point = this.edgeType.getPoint(0.5, viaNode); |
|
|
|
ctx.save(); |
|
|
|
|
|
|
|
// if the label has to be rotated:
|
|
|
@ -361,7 +340,7 @@ class Edge { |
|
|
|
var xObj = obj.left; |
|
|
|
var yObj = obj.top; |
|
|
|
|
|
|
|
var dist = this._getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj); |
|
|
|
var dist = this.edgeType.getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj); |
|
|
|
|
|
|
|
return (dist < distMax); |
|
|
|
} |
|
|
@ -371,380 +350,6 @@ class Edge { |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
_getColor(ctx) { |
|
|
|
var colorObj = this.options.color; |
|
|
|
|
|
|
|
if (colorObj.inherit.enabled === true) { |
|
|
|
if (colorObj.inherit.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.color.opacity); |
|
|
|
toColor = util.overrideOpacity(this.to.options.color.border, this.options.color.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); |
|
|
|
|
|
|
|
// -------------------- this returns -------------------- //
|
|
|
|
return grd; |
|
|
|
} |
|
|
|
|
|
|
|
if (this.colorDirty === true) { |
|
|
|
if (colorObj.inherit.source == "to") { |
|
|
|
colorObj.highlight = this.to.options.color.highlight.border; |
|
|
|
colorObj.hover = this.to.options.color.hover.border; |
|
|
|
colorObj.color = util.overrideOpacity(this.to.options.color.border, this.options.color.opacity); |
|
|
|
} |
|
|
|
else { // (this.options.color.inherit.source == "from") {
|
|
|
|
colorObj.highlight = this.from.options.color.highlight.border; |
|
|
|
colorObj.hover = this.from.options.color.hover.border; |
|
|
|
colorObj.color = util.overrideOpacity(this.from.options.color.border, this.options.color.opacity); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// if color inherit is on and gradients are used, the function has already returned by now.
|
|
|
|
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(); |
|
|
|
var via; |
|
|
|
|
|
|
|
if (this.from != this.to) { |
|
|
|
// draw line
|
|
|
|
via = this._line(ctx); |
|
|
|
} |
|
|
|
else { |
|
|
|
var x, y; |
|
|
|
var radius = this.options.selfReferenceSize; |
|
|
|
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); |
|
|
|
} |
|
|
|
|
|
|
|
return via; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* 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 |
|
|
|
*/ |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* Rotates the canvas so the text is most readable |
|
|
|
* @param {CanvasRenderingContext2D} ctx |
|
|
@ -764,94 +369,6 @@ class Edge { |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* Redraw a edge as a dashes 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.dashes.length !== undefined && this.options.dashes.gap !== undefined) { |
|
|
|
pattern = [this.options.dashes.length, this.options.dashes.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 dashes line
|
|
|
|
ctx.beginPath(); |
|
|
|
ctx.lineCap = 'round'; |
|
|
|
if (this.options.dashes.altLength !== undefined) //If an alt dash value has been set add to the array this value
|
|
|
|
{ |
|
|
|
ctx.dashesLine(this.from.x, this.from.y, this.to.x, this.to.y, |
|
|
|
[this.options.dashes.length, this.options.dashes.gap, this.options.dashes.altLength, this.options.dashes.gap]); |
|
|
|
} |
|
|
|
else if (this.options.dashes.length !== undefined && this.options.dashes.gap !== undefined) //If a dash and gap value has been set add to the array this value
|
|
|
|
{ |
|
|
|
ctx.dashesLine(this.from.x, this.from.y, this.to.x, this.to.y, |
|
|
|
[this.options.dashes.length, this.options.dashes.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(); |
|
|
|
} |
|
|
|
|
|
|
|
return via; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* Combined function of pointOnLine and pointOnBezier. This gives the coordinates of a point on the line at a certain percentage of the way |
|
|
|
* @param percentage |
|
|
|
* @param via |
|
|
|
* @returns {{x: number, y: number}} |
|
|
|
* @private |
|
|
|
*/ |
|
|
|
_pointOnEdge(percentage, via = this._getViaCoordinates()) { |
|
|
|
if (this.options.smooth.enabled == false) { |
|
|
|
return { |
|
|
|
x: (1 - percentage) * this.from.x + percentage * this.to.x, |
|
|
|
y: (1 - percentage) * this.from.y + percentage * this.to.y |
|
|
|
} |
|
|
|
} |
|
|
|
else { |
|
|
|
var t = percentage; |
|
|
|
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}; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Get a point on a circle |
|
|
|
* @param {Number} x |
|
|
@ -869,121 +386,7 @@ class Edge { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* This function uses binary search to look for the point where the bezier curve crosses the border of the node. |
|
|
|
* |
|
|
|
* @param nearNode |
|
|
|
* @param ctx |
|
|
|
* @param viaNode |
|
|
|
* @param nearNode |
|
|
|
* @param ctx |
|
|
|
* @param viaNode |
|
|
|
* @param nearNode |
|
|
|
* @param ctx |
|
|
|
* @param viaNode |
|
|
|
*/ |
|
|
|
_findBorderPositionBezier(nearNode, ctx, viaNode = this._getViaCoordinates()) { |
|
|
|
var maxIterations = 10; |
|
|
|
var iteration = 0; |
|
|
|
var low = 0; |
|
|
|
var high = 1; |
|
|
|
var pos, angle, distanceToBorder, distanceToPoint, difference; |
|
|
|
var threshold = 0.2; |
|
|
|
var node = this.to; |
|
|
|
var from = false; |
|
|
|
if (nearNode.id === this.from.id) { |
|
|
|
node = this.from; |
|
|
|
from = true; |
|
|
|
} |
|
|
|
|
|
|
|
while (low <= high && iteration < maxIterations) { |
|
|
|
var middle = (low + high) * 0.5; |
|
|
|
|
|
|
|
pos = this._pointOnEdge(middle, viaNode); |
|
|
|
angle = Math.atan2((node.y - pos.y), (node.x - pos.x)); |
|
|
|
distanceToBorder = node.distanceToBorder(ctx, angle); |
|
|
|
distanceToPoint = Math.sqrt(Math.pow(pos.x - node.x, 2) + Math.pow(pos.y - node.y, 2)); |
|
|
|
difference = distanceToBorder - distanceToPoint; |
|
|
|
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; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* This function uses binary search to look for the point where the circle crosses the border of the node. |
|
|
|
* @param x |
|
|
|
* @param y |
|
|
|
* @param radius |
|
|
|
* @param node |
|
|
|
* @param low |
|
|
|
* @param high |
|
|
|
* @param direction |
|
|
|
* @param ctx |
|
|
|
* @returns {*} |
|
|
|
* @private |
|
|
|
*/ |
|
|
|
_findBorderPositionCircle(x,y,radius,node,low,high,direction,ctx) { |
|
|
|
var maxIterations = 10; |
|
|
|
var iteration = 0; |
|
|
|
var pos, angle, distanceToBorder, distanceToPoint, difference; |
|
|
|
var threshold = 0.05; |
|
|
|
|
|
|
|
while (low <= high && iteration < maxIterations) { |
|
|
|
var middle = (low + high) * 0.5; |
|
|
|
|
|
|
|
pos = this._pointOnCircle(x,y,radius,middle); |
|
|
|
angle = Math.atan2((node.y - pos.y), (node.x - pos.x)); |
|
|
|
distanceToBorder = node.distanceToBorder(ctx, angle); |
|
|
|
distanceToPoint = Math.sqrt(Math.pow(pos.x - node.x, 2) + Math.pow(pos.y - node.y, 2)); |
|
|
|
difference = distanceToBorder - distanceToPoint; |
|
|
|
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 (direction > 0) { |
|
|
|
low = middle; |
|
|
|
} |
|
|
|
else { |
|
|
|
high = middle; |
|
|
|
} |
|
|
|
} |
|
|
|
else { |
|
|
|
if (direction > 0) { |
|
|
|
high = middle; |
|
|
|
} |
|
|
|
else { |
|
|
|
low = middle; |
|
|
|
} |
|
|
|
} |
|
|
|
iteration++; |
|
|
|
} |
|
|
|
pos.t = middle; |
|
|
|
|
|
|
|
return pos; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* |
|
|
@ -993,18 +396,18 @@ class Edge { |
|
|
|
*/ |
|
|
|
_drawArrowHead(ctx,position,viaNode) { |
|
|
|
// set style
|
|
|
|
ctx.strokeStyle = this._getColor(ctx); |
|
|
|
ctx.strokeStyle = this.edgeType.getColor(ctx); |
|
|
|
ctx.fillStyle = ctx.strokeStyle; |
|
|
|
ctx.lineWidth = this._getLineWidth(); |
|
|
|
ctx.lineWidth = this.edgeType.getLineWidth(); |
|
|
|
|
|
|
|
// set vars
|
|
|
|
var angle; |
|
|
|
var length; |
|
|
|
var arrowPos; |
|
|
|
var node1; |
|
|
|
var node2; |
|
|
|
var guideOffset; |
|
|
|
var scaleFactor; |
|
|
|
// set lets
|
|
|
|
let angle; |
|
|
|
let length; |
|
|
|
let arrowPos; |
|
|
|
let node1; |
|
|
|
let node2; |
|
|
|
let guideOffset; |
|
|
|
let scaleFactor; |
|
|
|
|
|
|
|
if (position == 'from') { |
|
|
|
node1 = this.from; |
|
|
@ -1029,26 +432,18 @@ class Edge { |
|
|
|
if (position !== 'middle') { |
|
|
|
// draw arrow head
|
|
|
|
if (this.options.smooth.enabled == true) { |
|
|
|
arrowPos = this._findBorderPositionBezier(node1, ctx, viaNode); |
|
|
|
var guidePos = this._pointOnEdge(Math.max(0.0,Math.min(1.0,arrowPos.t + guideOffset)), viaNode); |
|
|
|
arrowPos = this.edgeType.findBorderPosition(node1, ctx, {via:viaNode}); |
|
|
|
let guidePos = this.edgeType.getPoint(Math.max(0.0,Math.min(1.0,arrowPos.t + guideOffset)), viaNode); |
|
|
|
angle = Math.atan2((arrowPos.y - guidePos.y), (arrowPos.x - guidePos.x)); |
|
|
|
} |
|
|
|
else { |
|
|
|
angle = Math.atan2((node1.y - node2.y), (node1.x - node2.x)); |
|
|
|
var dx = (node1.x - node2.x); |
|
|
|
var dy = (node1.y - node2.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) * node2.x + toBorderPoint * node1.x; |
|
|
|
arrowPos.y = (1 - toBorderPoint) * node2.y + toBorderPoint * node1.y; |
|
|
|
arrowPos = this.edgeType.findBorderPosition(node1, ctx); |
|
|
|
} |
|
|
|
} |
|
|
|
else { |
|
|
|
angle = Math.atan2((node1.y - node2.y), (node1.x - node2.x)); |
|
|
|
arrowPos = this._pointOnEdge(0.6, viaNode); // this is 0.6 to account for the size of the arrow.
|
|
|
|
arrowPos = this.edgeType.getPoint(0.6, viaNode); // this is 0.6 to account for the size of the arrow.
|
|
|
|
} |
|
|
|
// draw arrow at the end of the line
|
|
|
|
length = (10 + 5 * this.options.width) * scaleFactor; |
|
|
@ -1058,9 +453,9 @@ class Edge { |
|
|
|
} |
|
|
|
else { |
|
|
|
// draw circle
|
|
|
|
var angle, point; |
|
|
|
var x, y; |
|
|
|
var radius = this.options.selfReferenceSize; |
|
|
|
let angle, point; |
|
|
|
let x, y; |
|
|
|
let radius = this.options.selfReferenceSize; |
|
|
|
if (!node1.width) { |
|
|
|
node1.resize(ctx); |
|
|
|
} |
|
|
@ -1077,131 +472,26 @@ class Edge { |
|
|
|
|
|
|
|
|
|
|
|
if (position == 'from') { |
|
|
|
point = this._findBorderPositionCircle(x, y, radius, node1, 0.25, 0.6, -1, ctx); |
|
|
|
point = this.edgeType.findBorderPosition(x, y, radius, node1, 0.25, 0.6, -1, ctx); |
|
|
|
angle = point.t * -2 * Math.PI + 1.5 * Math.PI + 0.1 * Math.PI; |
|
|
|
} |
|
|
|
else if (position == 'to') { |
|
|
|
point = this._findBorderPositionCircle(x, y, radius, node1, 0.6, 0.8, 1, ctx); |
|
|
|
point = this.edgeType.findBorderPosition(x, y, radius, node1, 0.6, 0.8, 1, ctx); |
|
|
|
angle = point.t * -2 * Math.PI + 1.5 * Math.PI - 1.1 * Math.PI; |
|
|
|
} |
|
|
|
else { |
|
|
|
point = this._pointOnCircle(x,y,radius,0.175); |
|
|
|
point = this.edgeType.findBorderPosition(x,y,radius,0.175); |
|
|
|
angle = 3.9269908169872414; // == 0.175 * -2 * Math.PI + 1.5 * Math.PI + 0.1 * Math.PI;
|
|
|
|
} |
|
|
|
|
|
|
|
// draw the arrowhead
|
|
|
|
var length = (10 + 5 * this.options.width) * scaleFactor; |
|
|
|
let length = (10 + 5 * this.options.width) * scaleFactor; |
|
|
|
ctx.arrow(point.x, point.y, angle, length); |
|
|
|
ctx.fill(); |
|
|
|
ctx.stroke(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
* 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; |
|
|
|
var lastX = x1; |
|
|
|
var lastY = y1; |
|
|
|
for (i = 1; 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 = this.options.selfReferenceSize; |
|
|
|
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.labelModule.size.left < x3 && |
|
|
|
this.labelModule.size.left + this.labelModule.size.width > x3 && |
|
|
|
this.labelModule.size.top < y3 && |
|
|
|
this.labelModule.size.top + this.labelModule.size.height > y3) { |
|
|
|
return 0; |
|
|
|
} |
|
|
|
else { |
|
|
|
return returnValue; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
_getDistanceToLine(x1, y1, x2, y2, x3, y3) { |
|
|
|
var px = x2 - x1; |
|
|
|
var py = y2 - y1; |
|
|
|
var something = px * px + py * py; |
|
|
|
var u = ((x3 - x1) * px + (y3 - y1) * py) / something; |
|
|
|
|
|
|
|
if (u > 1) { |
|
|
|
u = 1; |
|
|
|
} |
|
|
|
else if (u < 0) { |
|
|
|
u = 0; |
|
|
|
} |
|
|
|
|
|
|
|
var x = x1 + u * px; |
|
|
|
var y = y1 + u * py; |
|
|
|
var dx = x - x3; |
|
|
|
var 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 |
|
|
|
* |
|
|
@ -1220,16 +510,7 @@ class Edge { |
|
|
|
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; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|