|
|
- 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;
|