- let util = require("../../../../../util");
-
- /**
- * The Base Class for all edges.
- *
- * @class EdgeBase
- */
- class EdgeBase {
- /**
- * @param {Object} options
- * @param {Object} body
- * @param {Label} labelModule
- * @constructor EdgeBase
- */
- 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 {vis.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 {vis.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 {vis.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 {vis.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 {{toArrow: boolean, toArrowScale: (allOptions.edges.arrows.to.scaleFactor|{number}|allOptions.edges.arrows.middle.scaleFactor|allOptions.edges.arrows.from.scaleFactor|Array|number), toArrowType: *, middleArrow: boolean, middleArrowScale: (number|allOptions.edges.arrows.middle.scaleFactor|{number}|Array), middleArrowType: (allOptions.edges.arrows.middle.type|{string}|string|*), fromArrow: boolean, fromArrowScale: (allOptions.edges.arrows.to.scaleFactor|{number}|allOptions.edges.arrows.middle.scaleFactor|allOptions.edges.arrows.from.scaleFactor|Array|number), fromArrowType: *, arrowStrikethrough: (*|boolean|allOptions.edges.arrowStrikethrough|{boolean}), color: undefined, inheritsColor: (string|string|string|allOptions.edges.color.inherit|{string, boolean}|Array|*), opacity: *, hidden: *, length: *, shadow: *, shadowColor: *, shadowSize: *, shadowX: *, shadowY: *, dashes: (*|boolean|Array|allOptions.edges.dashes|{boolean, array}), width: *}} 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 {vis.Node} via
- * @param {Array} values
- * @returns {number}
- * @private
- */
- 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);
- }
-
- 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;
- }
- }
-
- /**
- *
- * @param {number} x1
- * @param {number} y1
- * @param {number} x2
- * @param {number} y2
- * @param {number} x3
- * @param {number} y3
- * @returns {number}
- * @private
- * @static
- */
- _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 {vis.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;
- }
- }
-
- 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 {{toArrow: boolean, toArrowScale: (allOptions.edges.arrows.to.scaleFactor|{number}|allOptions.edges.arrows.middle.scaleFactor|allOptions.edges.arrows.from.scaleFactor|Array|number), toArrowType: *, middleArrow: boolean, middleArrowScale: (number|allOptions.edges.arrows.middle.scaleFactor|{number}|Array), middleArrowType: (allOptions.edges.arrows.middle.type|{string}|string|*), fromArrow: boolean, fromArrowScale: (allOptions.edges.arrows.to.scaleFactor|{number}|allOptions.edges.arrows.middle.scaleFactor|allOptions.edges.arrows.from.scaleFactor|Array|number), fromArrowType: *, arrowStrikethrough: (*|boolean|allOptions.edges.arrowStrikethrough|{boolean}), color: undefined, inheritsColor: (string|string|string|allOptions.edges.color.inherit|{string, boolean}|Array|*), opacity: *, hidden: *, length: *, shadow: *, shadowColor: *, shadowSize: *, shadowX: *, shadowY: *, dashes: (*|boolean|Array|allOptions.edges.dashes|{boolean, array}), width: *}} 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;
-
- if (arrowData.type && arrowData.type.toLowerCase() === 'circle') {
- // draw circle at the end of the line
- ctx.circleEndpoint(arrowData.point.x, arrowData.point.y, arrowData.angle, arrowData.length);
- } else {
- // draw arrow at the end of the line
- ctx.arrowEndpoint(arrowData.point.x, arrowData.point.y, arrowData.angle, arrowData.length);
- }
-
- // draw shadow if enabled
- this.enableShadow(ctx, values);
- ctx.fill();
- // disable shadows for other elements.
- this.disableShadow(ctx, values);
- }
-
-
- /**
- *
- * @param {CanvasRenderingContext2D} ctx
- * @param {{toArrow: boolean, toArrowScale: (allOptions.edges.arrows.to.scaleFactor|{number}|allOptions.edges.arrows.middle.scaleFactor|allOptions.edges.arrows.from.scaleFactor|Array|number), toArrowType: *, middleArrow: boolean, middleArrowScale: (number|allOptions.edges.arrows.middle.scaleFactor|{number}|Array), middleArrowType: (allOptions.edges.arrows.middle.type|{string}|string|*), fromArrow: boolean, fromArrowScale: (allOptions.edges.arrows.to.scaleFactor|{number}|allOptions.edges.arrows.middle.scaleFactor|allOptions.edges.arrows.from.scaleFactor|Array|number), fromArrowType: *, arrowStrikethrough: (*|boolean|allOptions.edges.arrowStrikethrough|{boolean}), color: undefined, inheritsColor: (string|string|string|allOptions.edges.color.inherit|{string, boolean}|Array|*), opacity: *, hidden: *, length: *, shadow: *, shadowColor: *, shadowSize: *, shadowX: *, shadowY: *, dashes: (*|boolean|Array|allOptions.edges.dashes|{boolean, array}), width: *}} values
- * @static
- */
- 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 {{toArrow: boolean, toArrowScale: (allOptions.edges.arrows.to.scaleFactor|{number}|allOptions.edges.arrows.middle.scaleFactor|allOptions.edges.arrows.from.scaleFactor|Array|number), toArrowType: *, middleArrow: boolean, middleArrowScale: (number|allOptions.edges.arrows.middle.scaleFactor|{number}|Array), middleArrowType: (allOptions.edges.arrows.middle.type|{string}|string|*), fromArrow: boolean, fromArrowScale: (allOptions.edges.arrows.to.scaleFactor|{number}|allOptions.edges.arrows.middle.scaleFactor|allOptions.edges.arrows.from.scaleFactor|Array|number), fromArrowType: *, arrowStrikethrough: (*|boolean|allOptions.edges.arrowStrikethrough|{boolean}), color: undefined, inheritsColor: (string|string|string|allOptions.edges.color.inherit|{string, boolean}|Array|*), opacity: *, hidden: *, length: *, shadow: *, shadowColor: *, shadowSize: *, shadowX: *, shadowY: *, dashes: (*|boolean|Array|allOptions.edges.dashes|{boolean, array}), width: *}} values
- * @static
- */
- 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;
|