vis.js is a dynamic, browser-based visualization library
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

597 lines
16 KiB

let util = require("../../../../../util");
let EndPoints = require("./EndPoints").default;
/**
* The Base Class for all edges.
*
*/
class EdgeBase {
/**
* @param {Object} options
* @param {Object} body
* @param {Label} labelModule
*/
constructor(options, body, labelModule) {
this.body = body;
this.labelModule = labelModule;
this.options = {};
this.setOptions(options);
this.colorDirty = true;
this.color = {};
this.selectionWidth = 2;
this.hoverWidth = 1.5;
this.fromPoint = this.from;
this.toPoint = this.to;
}
/**
* Connects a node to itself
*/
connect() {
this.from = this.body.nodes[this.options.from];
this.to = this.body.nodes[this.options.to];
}
/**
*
* @returns {boolean} always false
*/
cleanup() {
return false;
}
/**
*
* @param {Object} options
*/
setOptions(options) {
this.options = options;
this.from = this.body.nodes[this.options.from];
this.to = this.body.nodes[this.options.to];
this.id = this.options.id;
}
/**
* 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
* @param {Array} values
* @param {boolean} selected
* @param {boolean} hover
* @param {Node} viaNode
* @private
*/
drawLine(ctx, values, selected, hover, viaNode) {
// set style
ctx.strokeStyle = this.getColor(ctx, values, selected, hover);
ctx.lineWidth = values.width;
if (values.dashes !== false) {
this._drawDashedLine(ctx, values, viaNode);
}
else {
this._drawLine(ctx, values, viaNode);
}
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {Array} values
* @param {Node} viaNode
* @param {{x: number, y: number}} [fromPoint]
* @param {{x: number, y: number}} [toPoint]
* @private
*/
_drawLine(ctx, values, viaNode, fromPoint, toPoint) {
if (this.from != this.to) {
// draw line
this._line(ctx, values, viaNode, fromPoint, toPoint);
}
else {
let [x,y,radius] = this._getCircleData(ctx);
this._circle(ctx, values, x, y, radius);
}
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {Array} values
* @param {Node} viaNode
* @param {{x: number, y: number}} [fromPoint] TODO: Remove in next major release
* @param {{x: number, y: number}} [toPoint] TODO: Remove in next major release
* @private
*/
_drawDashedLine(ctx, values, viaNode, fromPoint, toPoint) { // eslint-disable-line no-unused-vars
ctx.lineCap = 'round';
let pattern = [5,5];
if (Array.isArray(values.dashes) === true) {
pattern = values.dashes;
}
// only firefox and chrome support this method, else we use the legacy one.
if (ctx.setLineDash !== undefined) {
ctx.save();
// set dash settings for chrome or firefox
ctx.setLineDash(pattern);
ctx.lineDashOffset = 0;
// draw the line
if (this.from != this.to) {
// draw line
this._line(ctx, values, viaNode);
}
else {
let [x,y,radius] = this._getCircleData(ctx);
this._circle(ctx, values, x, y, radius);
}
// restore the dash settings.
ctx.setLineDash([0]);
ctx.lineDashOffset = 0;
ctx.restore();
}
else { // unsupporting smooth lines
if (this.from != this.to) {
// draw line
ctx.dashedLine(this.from.x, this.from.y, this.to.x, this.to.y, pattern);
}
else {
let [x,y,radius] = this._getCircleData(ctx);
this._circle(ctx, values, x, y, radius);
}
// draw shadow if enabled
this.enableShadow(ctx, values);
ctx.stroke();
// disable shadows for other elements.
this.disableShadow(ctx, values);
}
}
/**
*
* @param {Node} nearNode
* @param {CanvasRenderingContext2D} ctx
* @param {Object} options
* @returns {{x: number, y: number}}
*/
findBorderPosition(nearNode, ctx, options) {
if (this.from != this.to) {
return this._findBorderPosition(nearNode, ctx, options);
}
else {
return this._findBorderPositionCircle(nearNode, ctx, options);
}
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @returns {{from: ({x: number, y: number, t: number}|*), to: ({x: number, y: number, t: number}|*)}}
*/
findBorderPositions(ctx) {
let from = {};
let to = {};
if (this.from != this.to) {
from = this._findBorderPosition(this.from, ctx);
to = this._findBorderPosition(this.to, ctx);
}
else {
let [x,y] = this._getCircleData(ctx).slice(0, 2);
from = this._findBorderPositionCircle(this.from, ctx, {x, y, low:0.25, high:0.6, direction:-1});
to = this._findBorderPositionCircle(this.from, ctx, {x, y, low:0.6, high:0.8, direction:1});
}
return {from, to};
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @returns {Array.<number>} x, y, radius
* @private
*/
_getCircleData(ctx) {
let x, y;
let node = this.from;
let radius = this.options.selfReferenceSize;
if (ctx !== undefined) {
if (node.shape.width === undefined) {
node.shape.resize(ctx);
}
}
// get circle coordinates
if (node.shape.width > node.shape.height) {
x = node.x + node.shape.width * 0.5;
y = node.y - radius;
}
else {
x = node.x + radius;
y = node.y - node.shape.height * 0.5;
}
return [x,y,radius];
}
/**
* 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) {
let angle = percentage * 2 * Math.PI;
return {
x: x + radius * Math.cos(angle),
y: y - radius * Math.sin(angle)
}
}
/**
* This function uses binary search to look for the point where the circle crosses the border of the node.
* @param {Node} node
* @param {CanvasRenderingContext2D} ctx
* @param {Object} options
* @returns {*}
* @private
*/
_findBorderPositionCircle(node, ctx, options) {
let x = options.x;
let y = options.y;
let low = options.low;
let high = options.high;
let direction = options.direction;
let maxIterations = 10;
let iteration = 0;
let radius = this.options.selfReferenceSize;
let pos, angle, distanceToBorder, distanceToPoint, difference;
let threshold = 0.05;
let middle = (low + high) * 0.5;
while (low <= high && iteration < maxIterations) {
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;
}
/**
* Get the line width of the edge. Depends on width and whether one of the
* connected nodes is selected.
* @param {boolean} selected
* @param {boolean} hover
* @returns {number} width
* @private
*/
getLineWidth(selected, hover) {
if (selected === true) {
return Math.max(this.selectionWidth, 0.3 / this.body.view.scale);
}
else {
if (hover === true) {
return Math.max(this.hoverWidth, 0.3 / this.body.view.scale);
}
else {
return Math.max(this.options.width, 0.3 / this.body.view.scale);
}
}
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {ArrowOptions} values
* @param {boolean} selected - Unused
* @param {boolean} hover - Unused
* @returns {string}
*/
getColor(ctx, values, selected, hover) { // eslint-disable-line no-unused-vars
if (values.inheritsColor !== false) {
// when this is a loop edge, just use the 'from' method
if ((values.inheritsColor === 'both') && (this.from.id !== this.to.id)) {
let grd = ctx.createLinearGradient(this.from.x, this.from.y, this.to.x, this.to.y);
let 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, values.opacity);
toColor = util.overrideOpacity(this.to.options.color.border, values.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 (values.inheritsColor === "to") {
return util.overrideOpacity(this.to.options.color.border, values.opacity);
} else { // "from"
return util.overrideOpacity(this.from.options.color.border, values.opacity);
}
} else {
return util.overrideOpacity(values.color, values.opacity);
}
}
/**
* Draw a line from a node to itself, a circle
*
* @param {CanvasRenderingContext2D} ctx
* @param {Array} values
* @param {number} x
* @param {number} y
* @param {number} radius
* @private
*/
_circle(ctx, values, x, y, radius) {
// draw shadow if enabled
this.enableShadow(ctx, values);
// draw a circle
ctx.beginPath();
ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
ctx.stroke();
// disable shadows for other elements.
this.disableShadow(ctx, values);
}
/**
* Calculate the distance between a point (x3,y3) and a line segment from (x1,y1) to (x2,y2).
* (x3,y3) is the point.
*
* 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
* @param {Node} via
* @param {Array} values
* @returns {number}
*/
getDistanceToEdge(x1, y1, x2, y2, x3, y3, via, values) { // eslint-disable-line no-unused-vars
let returnValue = 0;
if (this.from != this.to) {
returnValue = this._getDistanceToEdge(x1, y1, x2, y2, x3, y3, via)
}
else {
let [x,y,radius] = this._getCircleData(undefined);
let dx = x - x3;
let dy = y - y3;
returnValue = Math.abs(Math.sqrt(dx * dx + dy * dy) - radius);
}
return returnValue;
}
/**
*
* @param {number} x1
* @param {number} y1
* @param {number} x2
* @param {number} y2
* @param {number} x3
* @param {number} y3
* @returns {number}
* @private
*/
_getDistanceToLine(x1, y1, x2, y2, x3, y3) {
let px = x2 - x1;
let py = y2 - y1;
let something = px * px + py * py;
let u = ((x3 - x1) * px + (y3 - y1) * py) / something;
if (u > 1) {
u = 1;
}
else if (u < 0) {
u = 0;
}
let x = x1 + u * px;
let y = y1 + u * py;
let dx = x - x3;
let 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);
}
/**
* @param {CanvasRenderingContext2D} ctx
* @param {string} position
* @param {Node} viaNode
* @param {boolean} selected
* @param {boolean} hover
* @param {Array} values
* @returns {{point: *, core: {x: number, y: number}, angle: *, length: number, type: *}}
*/
getArrowData(ctx, position, viaNode, selected, hover, values) {
// set lets
let angle;
let arrowPoint;
let node1;
let node2;
let guideOffset;
let scaleFactor;
let type;
let lineWidth = values.width;
if (position === 'from') {
node1 = this.from;
node2 = this.to;
guideOffset = 0.1;
scaleFactor = values.fromArrowScale;
type = values.fromArrowType;
}
else if (position === 'to') {
node1 = this.to;
node2 = this.from;
guideOffset = -0.1;
scaleFactor = values.toArrowScale;
type = values.toArrowType;
}
else {
node1 = this.to;
node2 = this.from;
scaleFactor = values.middleArrowScale;
type = values.middleArrowType;
}
// if not connected to itself
if (node1 != node2) {
if (position !== 'middle') {
// draw arrow head
if (this.options.smooth.enabled === true) {
arrowPoint = this.findBorderPosition(node1, ctx, { via: viaNode });
let guidePos = this.getPoint(Math.max(0.0, Math.min(1.0, arrowPoint.t + guideOffset)), viaNode);
angle = Math.atan2((arrowPoint.y - guidePos.y), (arrowPoint.x - guidePos.x));
} else {
angle = Math.atan2((node1.y - node2.y), (node1.x - node2.x));
arrowPoint = this.findBorderPosition(node1, ctx);
}
} else {
angle = Math.atan2((node1.y - node2.y), (node1.x - node2.x));
arrowPoint = this.getPoint(0.5, viaNode); // this is 0.6 to account for the size of the arrow.
}
} else {
// draw circle
let [x,y,radius] = this._getCircleData(ctx);
if (position === 'from') {
arrowPoint = this.findBorderPosition(this.from, ctx, { x, y, low: 0.25, high: 0.6, direction: -1 });
angle = arrowPoint.t * -2 * Math.PI + 1.5 * Math.PI + 0.1 * Math.PI;
} else if (position === 'to') {
arrowPoint = this.findBorderPosition(this.from, ctx, { x, y, low: 0.6, high: 1.0, direction: 1 });
angle = arrowPoint.t * -2 * Math.PI + 1.5 * Math.PI - 1.1 * Math.PI;
} else {
arrowPoint = this._pointOnCircle(x, y, radius, 0.175);
angle = 3.9269908169872414; // === 0.175 * -2 * Math.PI + 1.5 * Math.PI + 0.1 * Math.PI;
}
}
if (position === 'middle' && scaleFactor < 0) lineWidth *= -1; // reversed middle arrow
let length = 15 * scaleFactor + 3 * lineWidth; // 3* lineWidth is the width of the edge.
var xi = arrowPoint.x - length * 0.9 * Math.cos(angle);
var yi = arrowPoint.y - length * 0.9 * Math.sin(angle);
let arrowCore = { x: xi, y: yi };
return { point: arrowPoint, core: arrowCore, angle: angle, length: length, type: type };
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {ArrowOptions} values
* @param {boolean} selected
* @param {boolean} hover
* @param {Object} arrowData
*/
drawArrowHead(ctx, values, selected, hover, arrowData) {
// set style
ctx.strokeStyle = this.getColor(ctx, values, selected, hover);
ctx.fillStyle = ctx.strokeStyle;
ctx.lineWidth = values.width;
EndPoints.draw(ctx, arrowData);
// draw shadow if enabled
this.enableShadow(ctx, values);
ctx.fill();
// disable shadows for other elements.
this.disableShadow(ctx, values);
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {ArrowOptions} values
*/
enableShadow(ctx, values) {
if (values.shadow === true) {
ctx.shadowColor = values.shadowColor;
ctx.shadowBlur = values.shadowSize;
ctx.shadowOffsetX = values.shadowX;
ctx.shadowOffsetY = values.shadowY;
}
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {ArrowOptions} values
*/
disableShadow(ctx, values) {
if (values.shadow === true) {
ctx.shadowColor = 'rgba(0,0,0,0)';
ctx.shadowBlur = 0;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
}
}
}
export default EdgeBase;