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