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.

1358 lines
40 KiB

10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
  1. var util = require('../util');
  2. var Node = require('./Node');
  3. /**
  4. * @class Edge
  5. *
  6. * A edge connects two nodes
  7. * @param {Object} properties Object with properties. Must contain
  8. * At least properties from and to.
  9. * Available properties: from (number),
  10. * to (number), label (string, color (string),
  11. * width (number), style (string),
  12. * length (number), title (string)
  13. * @param {Network} network A Network object, used to find and edge to
  14. * nodes.
  15. * @param {Object} constants An object with default values for
  16. * example for the color
  17. */
  18. function Edge (properties, network, networkConstants) {
  19. if (!network) {
  20. throw "No network provided";
  21. }
  22. var fields = ['edges','physics'];
  23. var constants = util.selectiveBridgeObject(fields,networkConstants);
  24. this.options = constants.edges;
  25. this.physics = constants.physics;
  26. this.options['smoothCurves'] = networkConstants['smoothCurves'];
  27. this.network = network;
  28. // initialize variables
  29. this.id = undefined;
  30. this.fromId = undefined;
  31. this.toId = undefined;
  32. this.title = undefined;
  33. this.widthSelected = this.options.width * this.options.widthSelectionMultiplier;
  34. this.value = undefined;
  35. this.selected = false;
  36. this.hover = false;
  37. this.labelDimensions = {top:0,left:0,width:0,height:0,yLine:0}; // could be cached
  38. this.dirtyLabel = true;
  39. this.colorDirty = true;
  40. this.from = null; // a node
  41. this.to = null; // a node
  42. this.via = null; // a temp node
  43. this.fromBackup = null; // used to clean up after reconnect
  44. this.toBackup = null;; // used to clean up after reconnect
  45. // we use this to be able to reconnect the edge to a cluster if its node is put into a cluster
  46. // by storing the original information we can revert to the original connection when the cluser is opened.
  47. this.originalFromId = [];
  48. this.originalToId = [];
  49. this.connected = false;
  50. this.widthFixed = false;
  51. this.lengthFixed = false;
  52. this.setProperties(properties);
  53. this.controlNodesEnabled = false;
  54. this.controlNodes = {from:null, to:null, positions:{}};
  55. this.connectedNode = null;
  56. }
  57. /**
  58. * Set or overwrite properties for the edge
  59. * @param {Object} properties an object with properties
  60. * @param {Object} constants and object with default, global properties
  61. */
  62. Edge.prototype.setProperties = function(properties) {
  63. this.colorDirty = true;
  64. if (!properties) {
  65. return;
  66. }
  67. var fields = ['style','fontSize','fontFace','fontColor','fontFill','fontStrokeWidth','fontStrokeColor','width',
  68. 'widthSelectionMultiplier','hoverWidth','arrowScaleFactor','dash','inheritColor','labelAlignment', 'opacity'
  69. ];
  70. util.selectiveDeepExtend(fields, this.options, properties);
  71. if (properties.from !== undefined) {this.fromId = properties.from;}
  72. if (properties.to !== undefined) {this.toId = properties.to;}
  73. if (properties.id !== undefined) {this.id = properties.id;}
  74. if (properties.label !== undefined) {this.label = properties.label; this.dirtyLabel = true;}
  75. if (properties.title !== undefined) {this.title = properties.title;}
  76. if (properties.value !== undefined) {this.value = properties.value;}
  77. if (properties.length !== undefined) {this.physics.springLength = properties.length;}
  78. if (properties.color !== undefined) {
  79. this.options.inheritColor = false;
  80. if (util.isString(properties.color)) {
  81. this.options.color.color = properties.color;
  82. this.options.color.highlight = properties.color;
  83. }
  84. else {
  85. if (properties.color.color !== undefined) {this.options.color.color = properties.color.color;}
  86. if (properties.color.highlight !== undefined) {this.options.color.highlight = properties.color.highlight;}
  87. if (properties.color.hover !== undefined) {this.options.color.hover = properties.color.hover;}
  88. }
  89. }
  90. // A node is connected when it has a from and to node.
  91. this.connect();
  92. this.widthFixed = this.widthFixed || (properties.width !== undefined);
  93. this.lengthFixed = this.lengthFixed || (properties.length !== undefined);
  94. this.widthSelected = this.options.width* this.options.widthSelectionMultiplier;
  95. // set draw method based on style
  96. switch (this.options.style) {
  97. case 'line': this.draw = this._drawLine; break;
  98. case 'arrow': this.draw = this._drawArrow; break;
  99. case 'arrow-center': this.draw = this._drawArrowCenter; break;
  100. case 'dash-line': this.draw = this._drawDashLine; break;
  101. default: this.draw = this._drawLine; break;
  102. }
  103. };
  104. /**
  105. * Connect an edge to its nodes
  106. */
  107. Edge.prototype.connect = function () {
  108. this.disconnect();
  109. this.from = this.network.nodes[this.fromId] || null;
  110. this.to = this.network.nodes[this.toId] || null;
  111. this.connected = (this.from && this.to);
  112. if (this.connected) {
  113. this.from.attachEdge(this);
  114. this.to.attachEdge(this);
  115. }
  116. else {
  117. if (this.from) {
  118. this.from.detachEdge(this);
  119. }
  120. if (this.to) {
  121. this.to.detachEdge(this);
  122. }
  123. }
  124. };
  125. /**
  126. * Disconnect an edge from its nodes
  127. */
  128. Edge.prototype.disconnect = function () {
  129. if (this.from) {
  130. this.from.detachEdge(this);
  131. this.from = null;
  132. }
  133. if (this.to) {
  134. this.to.detachEdge(this);
  135. this.to = null;
  136. }
  137. this.connected = false;
  138. };
  139. /**
  140. * get the title of this edge.
  141. * @return {string} title The title of the edge, or undefined when no title
  142. * has been set.
  143. */
  144. Edge.prototype.getTitle = function() {
  145. return typeof this.title === "function" ? this.title() : this.title;
  146. };
  147. /**
  148. * Retrieve the value of the edge. Can be undefined
  149. * @return {Number} value
  150. */
  151. Edge.prototype.getValue = function() {
  152. return this.value;
  153. };
  154. /**
  155. * Adjust the value range of the edge. The edge will adjust it's width
  156. * based on its value.
  157. * @param {Number} min
  158. * @param {Number} max
  159. */
  160. Edge.prototype.setValueRange = function(min, max) {
  161. if (!this.widthFixed && this.value !== undefined) {
  162. var scale = (this.options.widthMax - this.options.widthMin) / (max - min);
  163. this.options.width= (this.value - min) * scale + this.options.widthMin;
  164. this.widthSelected = this.options.width* this.options.widthSelectionMultiplier;
  165. }
  166. };
  167. /**
  168. * Redraw a edge
  169. * Draw this edge in the given canvas
  170. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  171. * @param {CanvasRenderingContext2D} ctx
  172. */
  173. Edge.prototype.draw = function(ctx) {
  174. throw "Method draw not initialized in edge";
  175. };
  176. /**
  177. * Check if this object is overlapping with the provided object
  178. * @param {Object} obj an object with parameters left, top
  179. * @return {boolean} True if location is located on the edge
  180. */
  181. Edge.prototype.isOverlappingWith = function(obj) {
  182. if (this.connected) {
  183. var distMax = 10;
  184. var xFrom = this.from.x;
  185. var yFrom = this.from.y;
  186. var xTo = this.to.x;
  187. var yTo = this.to.y;
  188. var xObj = obj.left;
  189. var yObj = obj.top;
  190. var dist = this._getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj);
  191. return (dist < distMax);
  192. }
  193. else {
  194. return false
  195. }
  196. };
  197. Edge.prototype._getColor = function() {
  198. var colorObj = this.options.color;
  199. if (this.colorDirty === true) {
  200. if (this.options.inheritColor == "to") {
  201. colorObj = {
  202. highlight: this.to.options.color.highlight.border,
  203. hover: this.to.options.color.hover.border,
  204. color: util.overrideOpacity(this.from.options.color.border, this.options.opacity)
  205. };
  206. }
  207. else if (this.options.inheritColor == "from" || this.options.inheritColor == true) {
  208. colorObj = {
  209. highlight: this.from.options.color.highlight.border,
  210. hover: this.from.options.color.hover.border,
  211. color: util.overrideOpacity(this.from.options.color.border, this.options.opacity)
  212. };
  213. }
  214. this.options.color = colorObj;
  215. this.colorDirty = false;
  216. }
  217. if (this.selected == true) {return colorObj.highlight;}
  218. else if (this.hover == true) {return colorObj.hover;}
  219. else {return colorObj.color;}
  220. };
  221. /**
  222. * Redraw a edge as a line
  223. * Draw this edge in the given canvas
  224. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  225. * @param {CanvasRenderingContext2D} ctx
  226. * @private
  227. */
  228. Edge.prototype._drawLine = function(ctx) {
  229. // set style
  230. ctx.strokeStyle = this._getColor();
  231. ctx.lineWidth = this._getLineWidth();
  232. if (this.from != this.to) {
  233. // draw line
  234. var via = this._line(ctx);
  235. // draw label
  236. var point;
  237. if (this.label) {
  238. if (this.options.smoothCurves.enabled == true && via != null) {
  239. var midpointX = 0.5*(0.5*(this.from.x + via.x) + 0.5*(this.to.x + via.x));
  240. var midpointY = 0.5*(0.5*(this.from.y + via.y) + 0.5*(this.to.y + via.y));
  241. point = {x:midpointX, y:midpointY};
  242. }
  243. else {
  244. point = this._pointOnLine(0.5);
  245. }
  246. this._label(ctx, this.label, point.x, point.y);
  247. }
  248. }
  249. else {
  250. var x, y;
  251. var radius = this.physics.springLength / 4;
  252. var node = this.from;
  253. if (!node.width) {
  254. node.resize(ctx);
  255. }
  256. if (node.width > node.height) {
  257. x = node.x + node.width / 2;
  258. y = node.y - radius;
  259. }
  260. else {
  261. x = node.x + radius;
  262. y = node.y - node.height / 2;
  263. }
  264. this._circle(ctx, x, y, radius);
  265. point = this._pointOnCircle(x, y, radius, 0.5);
  266. this._label(ctx, this.label, point.x, point.y);
  267. }
  268. };
  269. /**
  270. * Get the line width of the edge. Depends on width and whether one of the
  271. * connected nodes is selected.
  272. * @return {Number} width
  273. * @private
  274. */
  275. Edge.prototype._getLineWidth = function() {
  276. if (this.selected == true) {
  277. return Math.max(Math.min(this.widthSelected, this.options.widthMax), 0.3*this.networkScaleInv);
  278. }
  279. else {
  280. if (this.hover == true) {
  281. return Math.max(Math.min(this.options.hoverWidth, this.options.widthMax), 0.3*this.networkScaleInv);
  282. }
  283. else {
  284. return Math.max(this.options.width, 0.3*this.networkScaleInv);
  285. }
  286. }
  287. };
  288. Edge.prototype._getViaCoordinates = function () {
  289. if (this.options.smoothCurves.dynamic == true && this.options.smoothCurves.enabled == true ) {
  290. return this.via;
  291. }
  292. else if (this.options.smoothCurves.enabled == false) {
  293. return {x:0,y:0};
  294. }
  295. else {
  296. var xVia = null;
  297. var yVia = null;
  298. var factor = this.options.smoothCurves.roundness;
  299. var type = this.options.smoothCurves.type;
  300. var dx = Math.abs(this.from.x - this.to.x);
  301. var dy = Math.abs(this.from.y - this.to.y);
  302. if (type == 'discrete' || type == 'diagonalCross') {
  303. if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) {
  304. if (this.from.y > this.to.y) {
  305. if (this.from.x < this.to.x) {
  306. xVia = this.from.x + factor * dy;
  307. yVia = this.from.y - factor * dy;
  308. }
  309. else if (this.from.x > this.to.x) {
  310. xVia = this.from.x - factor * dy;
  311. yVia = this.from.y - factor * dy;
  312. }
  313. }
  314. else if (this.from.y < this.to.y) {
  315. if (this.from.x < this.to.x) {
  316. xVia = this.from.x + factor * dy;
  317. yVia = this.from.y + factor * dy;
  318. }
  319. else if (this.from.x > this.to.x) {
  320. xVia = this.from.x - factor * dy;
  321. yVia = this.from.y + factor * dy;
  322. }
  323. }
  324. if (type == "discrete") {
  325. xVia = dx < factor * dy ? this.from.x : xVia;
  326. }
  327. }
  328. else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) {
  329. if (this.from.y > this.to.y) {
  330. if (this.from.x < this.to.x) {
  331. xVia = this.from.x + factor * dx;
  332. yVia = this.from.y - factor * dx;
  333. }
  334. else if (this.from.x > this.to.x) {
  335. xVia = this.from.x - factor * dx;
  336. yVia = this.from.y - factor * dx;
  337. }
  338. }
  339. else if (this.from.y < this.to.y) {
  340. if (this.from.x < this.to.x) {
  341. xVia = this.from.x + factor * dx;
  342. yVia = this.from.y + factor * dx;
  343. }
  344. else if (this.from.x > this.to.x) {
  345. xVia = this.from.x - factor * dx;
  346. yVia = this.from.y + factor * dx;
  347. }
  348. }
  349. if (type == "discrete") {
  350. yVia = dy < factor * dx ? this.from.y : yVia;
  351. }
  352. }
  353. }
  354. else if (type == "straightCross") {
  355. if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) { // up - down
  356. xVia = this.from.x;
  357. if (this.from.y < this.to.y) {
  358. yVia = this.to.y - (1 - factor) * dy;
  359. }
  360. else {
  361. yVia = this.to.y + (1 - factor) * dy;
  362. }
  363. }
  364. else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) { // left - right
  365. if (this.from.x < this.to.x) {
  366. xVia = this.to.x - (1 - factor) * dx;
  367. }
  368. else {
  369. xVia = this.to.x + (1 - factor) * dx;
  370. }
  371. yVia = this.from.y;
  372. }
  373. }
  374. else if (type == 'horizontal') {
  375. if (this.from.x < this.to.x) {
  376. xVia = this.to.x - (1 - factor) * dx;
  377. }
  378. else {
  379. xVia = this.to.x + (1 - factor) * dx;
  380. }
  381. yVia = this.from.y;
  382. }
  383. else if (type == 'vertical') {
  384. xVia = this.from.x;
  385. if (this.from.y < this.to.y) {
  386. yVia = this.to.y - (1 - factor) * dy;
  387. }
  388. else {
  389. yVia = this.to.y + (1 - factor) * dy;
  390. }
  391. }
  392. else { // continuous
  393. if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) {
  394. if (this.from.y > this.to.y) {
  395. if (this.from.x < this.to.x) {
  396. // console.log(1)
  397. xVia = this.from.x + factor * dy;
  398. yVia = this.from.y - factor * dy;
  399. xVia = this.to.x < xVia ? this.to.x : xVia;
  400. }
  401. else if (this.from.x > this.to.x) {
  402. // console.log(2)
  403. xVia = this.from.x - factor * dy;
  404. yVia = this.from.y - factor * dy;
  405. xVia = this.to.x > xVia ? this.to.x : xVia;
  406. }
  407. }
  408. else if (this.from.y < this.to.y) {
  409. if (this.from.x < this.to.x) {
  410. // console.log(3)
  411. xVia = this.from.x + factor * dy;
  412. yVia = this.from.y + factor * dy;
  413. xVia = this.to.x < xVia ? this.to.x : xVia;
  414. }
  415. else if (this.from.x > this.to.x) {
  416. // console.log(4, this.from.x, this.to.x)
  417. xVia = this.from.x - factor * dy;
  418. yVia = this.from.y + factor * dy;
  419. xVia = this.to.x > xVia ? this.to.x : xVia;
  420. }
  421. }
  422. }
  423. else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) {
  424. if (this.from.y > this.to.y) {
  425. if (this.from.x < this.to.x) {
  426. // console.log(5)
  427. xVia = this.from.x + factor * dx;
  428. yVia = this.from.y - factor * dx;
  429. yVia = this.to.y > yVia ? this.to.y : yVia;
  430. }
  431. else if (this.from.x > this.to.x) {
  432. // console.log(6)
  433. xVia = this.from.x - factor * dx;
  434. yVia = this.from.y - factor * dx;
  435. yVia = this.to.y > yVia ? this.to.y : yVia;
  436. }
  437. }
  438. else if (this.from.y < this.to.y) {
  439. if (this.from.x < this.to.x) {
  440. // console.log(7)
  441. xVia = this.from.x + factor * dx;
  442. yVia = this.from.y + factor * dx;
  443. yVia = this.to.y < yVia ? this.to.y : yVia;
  444. }
  445. else if (this.from.x > this.to.x) {
  446. // console.log(8)
  447. xVia = this.from.x - factor * dx;
  448. yVia = this.from.y + factor * dx;
  449. yVia = this.to.y < yVia ? this.to.y : yVia;
  450. }
  451. }
  452. }
  453. }
  454. return {x: xVia, y: yVia};
  455. }
  456. };
  457. /**
  458. * Draw a line between two nodes
  459. * @param {CanvasRenderingContext2D} ctx
  460. * @private
  461. */
  462. Edge.prototype._line = function (ctx) {
  463. // draw a straight line
  464. ctx.beginPath();
  465. ctx.moveTo(this.from.x, this.from.y);
  466. if (this.options.smoothCurves.enabled == true) {
  467. if (this.options.smoothCurves.dynamic == false) {
  468. var via = this._getViaCoordinates();
  469. if (via.x == null) {
  470. ctx.lineTo(this.to.x, this.to.y);
  471. ctx.stroke();
  472. return null;
  473. }
  474. else {
  475. // this.via.x = via.x;
  476. // this.via.y = via.y;
  477. ctx.quadraticCurveTo(via.x,via.y,this.to.x, this.to.y);
  478. ctx.stroke();
  479. return via;
  480. }
  481. }
  482. else {
  483. ctx.quadraticCurveTo(this.via.x,this.via.y,this.to.x, this.to.y);
  484. ctx.stroke();
  485. return this.via;
  486. }
  487. }
  488. else {
  489. ctx.lineTo(this.to.x, this.to.y);
  490. ctx.stroke();
  491. return null;
  492. }
  493. };
  494. /**
  495. * Draw a line from a node to itself, a circle
  496. * @param {CanvasRenderingContext2D} ctx
  497. * @param {Number} x
  498. * @param {Number} y
  499. * @param {Number} radius
  500. * @private
  501. */
  502. Edge.prototype._circle = function (ctx, x, y, radius) {
  503. // draw a circle
  504. ctx.beginPath();
  505. ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
  506. ctx.stroke();
  507. };
  508. /**
  509. * Draw label with white background and with the middle at (x, y)
  510. * @param {CanvasRenderingContext2D} ctx
  511. * @param {String} text
  512. * @param {Number} x
  513. * @param {Number} y
  514. * @private
  515. */
  516. Edge.prototype._label = function (ctx, text, x, y) {
  517. if (text) {
  518. ctx.font = ((this.from.selected || this.to.selected) ? "bold " : "") +
  519. this.options.fontSize + "px " + this.options.fontFace;
  520. var yLine;
  521. if (this.dirtyLabel == true) {
  522. var lines = String(text).split('\n');
  523. var lineCount = lines.length;
  524. var fontSize = Number(this.options.fontSize);
  525. yLine = y + (1 - lineCount) / 2 * fontSize;
  526. var width = ctx.measureText(lines[0]).width;
  527. for (var i = 1; i < lineCount; i++) {
  528. var lineWidth = ctx.measureText(lines[i]).width;
  529. width = lineWidth > width ? lineWidth : width;
  530. }
  531. var height = this.options.fontSize * lineCount;
  532. var left = x - width / 2;
  533. var top = y - height / 2;
  534. // cache
  535. this.labelDimensions = {top:top,left:left,width:width,height:height,yLine:yLine};
  536. }
  537. var yLine = this.labelDimensions.yLine;
  538. ctx.save();
  539. if (this.options.labelAlignment != "horizontal"){
  540. ctx.translate(x, yLine);
  541. this._rotateForLabelAlignment(ctx);
  542. x = 0;
  543. yLine = 0;
  544. }
  545. this._drawLabelRect(ctx);
  546. this._drawLabelText(ctx,x,yLine, lines, lineCount, fontSize);
  547. ctx.restore();
  548. }
  549. };
  550. /**
  551. * Rotates the canvas so the text is most readable
  552. * @param {CanvasRenderingContext2D} ctx
  553. * @private
  554. */
  555. Edge.prototype._rotateForLabelAlignment = function(ctx) {
  556. var dy = this.from.y - this.to.y;
  557. var dx = this.from.x - this.to.x;
  558. var angleInDegrees = Math.atan2(dy, dx);
  559. // rotate so label it is readable
  560. if((angleInDegrees < -1 && dx < 0) || (angleInDegrees > 0 && dx < 0)){
  561. angleInDegrees = angleInDegrees + Math.PI;
  562. }
  563. ctx.rotate(angleInDegrees);
  564. };
  565. /**
  566. * Draws the label rectangle
  567. * @param {CanvasRenderingContext2D} ctx
  568. * @param {String} labelAlignment
  569. * @private
  570. */
  571. Edge.prototype._drawLabelRect = function(ctx) {
  572. if (this.options.fontFill !== undefined && this.options.fontFill !== null && this.options.fontFill !== "none") {
  573. ctx.fillStyle = this.options.fontFill;
  574. var lineMargin = 2;
  575. if (this.options.labelAlignment == 'line-center') {
  576. ctx.fillRect(-this.labelDimensions.width * 0.5, -this.labelDimensions.height * 0.5, this.labelDimensions.width, this.labelDimensions.height);
  577. }
  578. else if (this.options.labelAlignment == 'line-above') {
  579. ctx.fillRect(-this.labelDimensions.width * 0.5, -(this.labelDimensions.height + lineMargin), this.labelDimensions.width, this.labelDimensions.height);
  580. }
  581. else if (this.options.labelAlignment == 'line-below') {
  582. ctx.fillRect(-this.labelDimensions.width * 0.5, lineMargin, this.labelDimensions.width, this.labelDimensions.height);
  583. }
  584. else {
  585. ctx.fillRect(this.labelDimensions.left, this.labelDimensions.top, this.labelDimensions.width, this.labelDimensions.height);
  586. }
  587. }
  588. };
  589. /**
  590. * Draws the label text
  591. * @param {CanvasRenderingContext2D} ctx
  592. * @param {Number} x
  593. * @param {Number} yLine
  594. * @param {Array} lines
  595. * @param {Number} lineCount
  596. * @param {Number} fontSize
  597. * @private
  598. */
  599. Edge.prototype._drawLabelText = function(ctx, x, yLine, lines, lineCount, fontSize) {
  600. // draw text
  601. ctx.fillStyle = this.options.fontColor || "black";
  602. ctx.textAlign = "center";
  603. // check for label alignment
  604. if (this.options.labelAlignment != 'horizontal') {
  605. var lineMargin = 2;
  606. if (this.options.labelAlignment == 'line-above') {
  607. ctx.textBaseline = "alphabetic";
  608. yLine -= 2 * lineMargin; // distance from edge, required because we use alphabetic. Alphabetic has less difference between browsers
  609. }
  610. else if (this.options.labelAlignment == 'line-below') {
  611. ctx.textBaseline = "hanging";
  612. yLine += 2 * lineMargin;// distance from edge, required because we use hanging. Hanging has less difference between browsers
  613. }
  614. else {
  615. ctx.textBaseline = "middle";
  616. }
  617. }
  618. else {
  619. ctx.textBaseline = "middle";
  620. }
  621. // check for strokeWidth
  622. if (this.options.fontStrokeWidth > 0){
  623. ctx.lineWidth = this.options.fontStrokeWidth;
  624. ctx.strokeStyle = this.options.fontStrokeColor;
  625. ctx.lineJoin = 'round';
  626. }
  627. for (var i = 0; i < lineCount; i++) {
  628. if(this.options.fontStrokeWidth > 0){
  629. ctx.strokeText(lines[i], x, yLine);
  630. }
  631. ctx.fillText(lines[i], x, yLine);
  632. yLine += fontSize;
  633. }
  634. };
  635. /**
  636. * Redraw a edge as a dashed line
  637. * Draw this edge in the given canvas
  638. * @author David Jordan
  639. * @date 2012-08-08
  640. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  641. * @param {CanvasRenderingContext2D} ctx
  642. * @private
  643. */
  644. Edge.prototype._drawDashLine = function(ctx) {
  645. // set style
  646. ctx.strokeStyle = this._getColor();
  647. ctx.lineWidth = this._getLineWidth();
  648. var via = null;
  649. // only firefox and chrome support this method, else we use the legacy one.
  650. if (ctx.setLineDash !== undefined) {
  651. ctx.save();
  652. // configure the dash pattern
  653. var pattern = [0];
  654. if (this.options.dash.length !== undefined && this.options.dash.gap !== undefined) {
  655. pattern = [this.options.dash.length,this.options.dash.gap];
  656. }
  657. else {
  658. pattern = [5,5];
  659. }
  660. // set dash settings for chrome or firefox
  661. ctx.setLineDash(pattern);
  662. ctx.lineDashOffset = 0;
  663. // draw the line
  664. via = this._line(ctx);
  665. // restore the dash settings.
  666. ctx.setLineDash([0]);
  667. ctx.lineDashOffset = 0;
  668. ctx.restore();
  669. }
  670. else { // unsupporting smooth lines
  671. // draw dashed line
  672. ctx.beginPath();
  673. ctx.lineCap = 'round';
  674. if (this.options.dash.altLength !== undefined) //If an alt dash value has been set add to the array this value
  675. {
  676. ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
  677. [this.options.dash.length,this.options.dash.gap,this.options.dash.altLength,this.options.dash.gap]);
  678. }
  679. 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
  680. {
  681. ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
  682. [this.options.dash.length,this.options.dash.gap]);
  683. }
  684. else //If all else fails draw a line
  685. {
  686. ctx.moveTo(this.from.x, this.from.y);
  687. ctx.lineTo(this.to.x, this.to.y);
  688. }
  689. ctx.stroke();
  690. }
  691. // draw label
  692. if (this.label) {
  693. var point;
  694. if (this.options.smoothCurves.enabled == true && via != null) {
  695. var midpointX = 0.5*(0.5*(this.from.x + via.x) + 0.5*(this.to.x + via.x));
  696. var midpointY = 0.5*(0.5*(this.from.y + via.y) + 0.5*(this.to.y + via.y));
  697. point = {x:midpointX, y:midpointY};
  698. }
  699. else {
  700. point = this._pointOnLine(0.5);
  701. }
  702. this._label(ctx, this.label, point.x, point.y);
  703. }
  704. };
  705. /**
  706. * Get a point on a line
  707. * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
  708. * @return {Object} point
  709. * @private
  710. */
  711. Edge.prototype._pointOnLine = function (percentage) {
  712. return {
  713. x: (1 - percentage) * this.from.x + percentage * this.to.x,
  714. y: (1 - percentage) * this.from.y + percentage * this.to.y
  715. }
  716. };
  717. /**
  718. * Get a point on a circle
  719. * @param {Number} x
  720. * @param {Number} y
  721. * @param {Number} radius
  722. * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
  723. * @return {Object} point
  724. * @private
  725. */
  726. Edge.prototype._pointOnCircle = function (x, y, radius, percentage) {
  727. var angle = (percentage - 3/8) * 2 * Math.PI;
  728. return {
  729. x: x + radius * Math.cos(angle),
  730. y: y - radius * Math.sin(angle)
  731. }
  732. };
  733. /**
  734. * Redraw a edge as a line with an arrow halfway the line
  735. * Draw this edge in the given canvas
  736. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  737. * @param {CanvasRenderingContext2D} ctx
  738. * @private
  739. */
  740. Edge.prototype._drawArrowCenter = function(ctx) {
  741. var point;
  742. // set style
  743. ctx.strokeStyle = this._getColor();
  744. ctx.fillStyle = ctx.strokeStyle;
  745. ctx.lineWidth = this._getLineWidth();
  746. if (this.from != this.to) {
  747. // draw line
  748. var via = this._line(ctx);
  749. var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  750. var length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor;
  751. // draw an arrow halfway the line
  752. if (this.options.smoothCurves.enabled == true && via != null) {
  753. var midpointX = 0.5*(0.5*(this.from.x + via.x) + 0.5*(this.to.x + via.x));
  754. var midpointY = 0.5*(0.5*(this.from.y + via.y) + 0.5*(this.to.y + via.y));
  755. point = {x:midpointX, y:midpointY};
  756. }
  757. else {
  758. point = this._pointOnLine(0.5);
  759. }
  760. ctx.arrow(point.x, point.y, angle, length);
  761. ctx.fill();
  762. ctx.stroke();
  763. // draw label
  764. if (this.label) {
  765. this._label(ctx, this.label, point.x, point.y);
  766. }
  767. }
  768. else {
  769. // draw circle
  770. var x, y;
  771. var radius = 0.25 * Math.max(100,this.physics.springLength);
  772. var node = this.from;
  773. if (!node.width) {
  774. node.resize(ctx);
  775. }
  776. if (node.width > node.height) {
  777. x = node.x + node.width * 0.5;
  778. y = node.y - radius;
  779. }
  780. else {
  781. x = node.x + radius;
  782. y = node.y - node.height * 0.5;
  783. }
  784. this._circle(ctx, x, y, radius);
  785. // draw all arrows
  786. var angle = 0.2 * Math.PI;
  787. var length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor;
  788. point = this._pointOnCircle(x, y, radius, 0.5);
  789. ctx.arrow(point.x, point.y, angle, length);
  790. ctx.fill();
  791. ctx.stroke();
  792. // draw label
  793. if (this.label) {
  794. point = this._pointOnCircle(x, y, radius, 0.5);
  795. this._label(ctx, this.label, point.x, point.y);
  796. }
  797. }
  798. };
  799. Edge.prototype._pointOnBezier = function(t) {
  800. var via = this._getViaCoordinates();
  801. var x = Math.pow(1-t,2)*this.from.x + (2*t*(1 - t))*via.x + Math.pow(t,2)*this.to.x;
  802. var y = Math.pow(1-t,2)*this.from.y + (2*t*(1 - t))*via.y + Math.pow(t,2)*this.to.y;
  803. return {x:x,y:y};
  804. }
  805. /**
  806. * This function uses binary search to look for the point where the bezier curve crosses the border of the node.
  807. *
  808. * @param from
  809. * @param ctx
  810. * @returns {*}
  811. * @private
  812. */
  813. Edge.prototype._findBorderPosition = function(from,ctx) {
  814. var maxIterations = 10;
  815. var iteration = 0;
  816. var low = 0;
  817. var high = 1;
  818. var pos,angle,distanceToBorder, distanceToNodes, difference;
  819. var threshold = 0.2;
  820. var node = this.to;
  821. if (from == true) {
  822. node = this.from;
  823. }
  824. while (low <= high && iteration < maxIterations) {
  825. var middle = (low + high) * 0.5;
  826. pos = this._pointOnBezier(middle);
  827. angle = Math.atan2((node.y - pos.y), (node.x - pos.x));
  828. distanceToBorder = node.distanceToBorder(ctx,angle);
  829. distanceToNodes = Math.sqrt(Math.pow(pos.x-node.x,2) + Math.pow(pos.y-node.y,2));
  830. difference = distanceToBorder - distanceToNodes;
  831. if (Math.abs(difference) < threshold) {
  832. break; // found
  833. }
  834. 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.
  835. if (from == false) {
  836. low = middle;
  837. }
  838. else {
  839. high = middle;
  840. }
  841. }
  842. else {
  843. if (from == false) {
  844. high = middle;
  845. }
  846. else {
  847. low = middle;
  848. }
  849. }
  850. iteration++;
  851. }
  852. pos.t = middle;
  853. return pos;
  854. };
  855. /**
  856. * Redraw a edge as a line with an arrow
  857. * Draw this edge in the given canvas
  858. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  859. * @param {CanvasRenderingContext2D} ctx
  860. * @private
  861. */
  862. Edge.prototype._drawArrow = function(ctx) {
  863. // set style
  864. ctx.strokeStyle = this._getColor();
  865. ctx.fillStyle = ctx.strokeStyle;
  866. ctx.lineWidth = this._getLineWidth();
  867. // set vars
  868. var angle, length, arrowPos;
  869. // if not connected to itself
  870. if (this.from != this.to) {
  871. // draw line
  872. this._line(ctx);
  873. // draw arrow head
  874. if (this.options.smoothCurves.enabled == true) {
  875. var via = this._getViaCoordinates();
  876. arrowPos = this._findBorderPosition(false, ctx);
  877. var guidePos = this._pointOnBezier(Math.max(0.0, arrowPos.t - 0.1))
  878. angle = Math.atan2((arrowPos.y - guidePos.y), (arrowPos.x - guidePos.x));
  879. }
  880. else {
  881. angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  882. var dx = (this.to.x - this.from.x);
  883. var dy = (this.to.y - this.from.y);
  884. var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
  885. var toBorderDist = this.to.distanceToBorder(ctx, angle);
  886. var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength;
  887. arrowPos = {};
  888. arrowPos.x = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x;
  889. arrowPos.y = (1 - toBorderPoint) * this.from.y + toBorderPoint * this.to.y;
  890. }
  891. // draw arrow at the end of the line
  892. length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor;
  893. ctx.arrow(arrowPos.x,arrowPos.y, angle, length);
  894. ctx.fill();
  895. ctx.stroke();
  896. // draw label
  897. if (this.label) {
  898. var point;
  899. if (this.options.smoothCurves.enabled == true && via != null) {
  900. point = this._pointOnBezier(0.5);
  901. }
  902. else {
  903. point = this._pointOnLine(0.5);
  904. }
  905. this._label(ctx, this.label, point.x, point.y);
  906. }
  907. }
  908. else {
  909. // draw circle
  910. var node = this.from;
  911. var x, y, arrow;
  912. var radius = 0.25 * Math.max(100,this.physics.springLength);
  913. if (!node.width) {
  914. node.resize(ctx);
  915. }
  916. if (node.width > node.height) {
  917. x = node.x + node.width * 0.5;
  918. y = node.y - radius;
  919. arrow = {
  920. x: x,
  921. y: node.y,
  922. angle: 0.9 * Math.PI
  923. };
  924. }
  925. else {
  926. x = node.x + radius;
  927. y = node.y - node.height * 0.5;
  928. arrow = {
  929. x: node.x,
  930. y: y,
  931. angle: 0.6 * Math.PI
  932. };
  933. }
  934. ctx.beginPath();
  935. // TODO: similarly, for a line without arrows, draw to the border of the nodes instead of the center
  936. ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
  937. ctx.stroke();
  938. // draw all arrows
  939. var length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor;
  940. ctx.arrow(arrow.x, arrow.y, arrow.angle, length);
  941. ctx.fill();
  942. ctx.stroke();
  943. // draw label
  944. if (this.label) {
  945. point = this._pointOnCircle(x, y, radius, 0.5);
  946. this._label(ctx, this.label, point.x, point.y);
  947. }
  948. }
  949. };
  950. /**
  951. * Calculate the distance between a point (x3,y3) and a line segment from
  952. * (x1,y1) to (x2,y2).
  953. * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment
  954. * @param {number} x1
  955. * @param {number} y1
  956. * @param {number} x2
  957. * @param {number} y2
  958. * @param {number} x3
  959. * @param {number} y3
  960. * @private
  961. */
  962. Edge.prototype._getDistanceToEdge = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point
  963. var returnValue = 0;
  964. if (this.from != this.to) {
  965. if (this.options.smoothCurves.enabled == true) {
  966. var xVia, yVia;
  967. if (this.options.smoothCurves.enabled == true && this.options.smoothCurves.dynamic == true) {
  968. xVia = this.via.x;
  969. yVia = this.via.y;
  970. }
  971. else {
  972. var via = this._getViaCoordinates();
  973. xVia = via.x;
  974. yVia = via.y;
  975. }
  976. var minDistance = 1e9;
  977. var distance;
  978. var i,t,x,y, lastX, lastY;
  979. for (i = 0; i < 10; i++) {
  980. t = 0.1*i;
  981. x = Math.pow(1-t,2)*x1 + (2*t*(1 - t))*xVia + Math.pow(t,2)*x2;
  982. y = Math.pow(1-t,2)*y1 + (2*t*(1 - t))*yVia + Math.pow(t,2)*y2;
  983. if (i > 0) {
  984. distance = this._getDistanceToLine(lastX,lastY,x,y, x3,y3);
  985. minDistance = distance < minDistance ? distance : minDistance;
  986. }
  987. lastX = x; lastY = y;
  988. }
  989. returnValue = minDistance;
  990. }
  991. else {
  992. returnValue = this._getDistanceToLine(x1,y1,x2,y2,x3,y3);
  993. }
  994. }
  995. else {
  996. var x, y, dx, dy;
  997. var radius = 0.25 * this.physics.springLength;
  998. var node = this.from;
  999. if (node.width > node.height) {
  1000. x = node.x + 0.5 * node.width;
  1001. y = node.y - radius;
  1002. }
  1003. else {
  1004. x = node.x + radius;
  1005. y = node.y - 0.5 * node.height;
  1006. }
  1007. dx = x - x3;
  1008. dy = y - y3;
  1009. returnValue = Math.abs(Math.sqrt(dx*dx + dy*dy) - radius);
  1010. }
  1011. if (this.labelDimensions.left < x3 &&
  1012. this.labelDimensions.left + this.labelDimensions.width > x3 &&
  1013. this.labelDimensions.top < y3 &&
  1014. this.labelDimensions.top + this.labelDimensions.height > y3) {
  1015. return 0;
  1016. }
  1017. else {
  1018. return returnValue;
  1019. }
  1020. };
  1021. Edge.prototype._getDistanceToLine = function(x1,y1,x2,y2,x3,y3) {
  1022. var px = x2-x1,
  1023. py = y2-y1,
  1024. something = px*px + py*py,
  1025. u = ((x3 - x1) * px + (y3 - y1) * py) / something;
  1026. if (u > 1) {
  1027. u = 1;
  1028. }
  1029. else if (u < 0) {
  1030. u = 0;
  1031. }
  1032. var x = x1 + u * px,
  1033. y = y1 + u * py,
  1034. dx = x - x3,
  1035. dy = y - y3;
  1036. //# Note: If the actual distance does not matter,
  1037. //# if you only want to compare what this function
  1038. //# returns to other results of this function, you
  1039. //# can just return the squared distance instead
  1040. //# (i.e. remove the sqrt) to gain a little performance
  1041. return Math.sqrt(dx*dx + dy*dy);
  1042. };
  1043. /**
  1044. * This allows the zoom level of the network to influence the rendering
  1045. *
  1046. * @param scale
  1047. */
  1048. Edge.prototype.setScale = function(scale) {
  1049. this.networkScaleInv = 1.0/scale;
  1050. };
  1051. Edge.prototype.select = function() {
  1052. this.selected = true;
  1053. };
  1054. Edge.prototype.unselect = function() {
  1055. this.selected = false;
  1056. };
  1057. Edge.prototype.positionBezierNode = function() {
  1058. if (this.via !== null && this.from !== null && this.to !== null) {
  1059. this.via.x = 0.5 * (this.from.x + this.to.x);
  1060. this.via.y = 0.5 * (this.from.y + this.to.y);
  1061. }
  1062. else if (this.via !== null) {
  1063. this.via.x = 0;
  1064. this.via.y = 0;
  1065. }
  1066. };
  1067. /**
  1068. * This function draws the control nodes for the manipulator.
  1069. * In order to enable this, only set the this.controlNodesEnabled to true.
  1070. * @param ctx
  1071. */
  1072. Edge.prototype._drawControlNodes = function(ctx) {
  1073. if (this.controlNodesEnabled == true) {
  1074. if (this.controlNodes.from === null && this.controlNodes.to === null) {
  1075. var nodeIdFrom = "edgeIdFrom:".concat(this.id);
  1076. var nodeIdTo = "edgeIdTo:".concat(this.id);
  1077. var constants = {
  1078. nodes:{group:'', radius:7, borderWidth:2, borderWidthSelected: 2},
  1079. physics:{damping:0},
  1080. clustering: {maxNodeSizeIncrements: 0 ,nodeScaling: {width:0, height: 0, radius:0}}
  1081. };
  1082. this.controlNodes.from = new Node(
  1083. {id:nodeIdFrom,
  1084. shape:'dot',
  1085. color:{background:'#ff0000', border:'#3c3c3c', highlight: {background:'#07f968'}}
  1086. },{},{},constants);
  1087. this.controlNodes.to = new Node(
  1088. {id:nodeIdTo,
  1089. shape:'dot',
  1090. color:{background:'#ff0000', border:'#3c3c3c', highlight: {background:'#07f968'}}
  1091. },{},{},constants);
  1092. }
  1093. this.controlNodes.positions = {};
  1094. if (this.controlNodes.from.selected == false) {
  1095. this.controlNodes.positions.from = this.getControlNodeFromPosition(ctx);
  1096. this.controlNodes.from.x = this.controlNodes.positions.from.x;
  1097. this.controlNodes.from.y = this.controlNodes.positions.from.y;
  1098. }
  1099. if (this.controlNodes.to.selected == false) {
  1100. this.controlNodes.positions.to = this.getControlNodeToPosition(ctx);
  1101. this.controlNodes.to.x = this.controlNodes.positions.to.x;
  1102. this.controlNodes.to.y = this.controlNodes.positions.to.y;
  1103. }
  1104. this.controlNodes.from.draw(ctx);
  1105. this.controlNodes.to.draw(ctx);
  1106. }
  1107. else {
  1108. this.controlNodes = {from:null, to:null, positions:{}};
  1109. }
  1110. };
  1111. /**
  1112. * Enable control nodes.
  1113. * @private
  1114. */
  1115. Edge.prototype._enableControlNodes = function() {
  1116. this.fromBackup = this.from;
  1117. this.toBackup = this.to;
  1118. this.controlNodesEnabled = true;
  1119. };
  1120. /**
  1121. * disable control nodes and remove from dynamicEdges from old node
  1122. * @private
  1123. */
  1124. Edge.prototype._disableControlNodes = function() {
  1125. this.fromId = this.from.id;
  1126. this.toId = this.to.id;
  1127. if (this.fromId != this.fromBackup.id) { // from was changed, remove edge from old 'from' node dynamic edges
  1128. this.fromBackup.detachEdge(this);
  1129. }
  1130. else if (this.toId != this.toBackup.id) { // to was changed, remove edge from old 'to' node dynamic edges
  1131. this.toBackup.detachEdge(this);
  1132. }
  1133. this.fromBackup = null;
  1134. this.toBackup = null;
  1135. this.controlNodesEnabled = false;
  1136. };
  1137. /**
  1138. * This checks if one of the control nodes is selected and if so, returns the control node object. Else it returns null.
  1139. * @param x
  1140. * @param y
  1141. * @returns {null}
  1142. * @private
  1143. */
  1144. Edge.prototype._getSelectedControlNode = function(x,y) {
  1145. var positions = this.controlNodes.positions;
  1146. var fromDistance = Math.sqrt(Math.pow(x - positions.from.x,2) + Math.pow(y - positions.from.y,2));
  1147. var toDistance = Math.sqrt(Math.pow(x - positions.to.x ,2) + Math.pow(y - positions.to.y ,2));
  1148. if (fromDistance < 15) {
  1149. this.connectedNode = this.from;
  1150. this.from = this.controlNodes.from;
  1151. return this.controlNodes.from;
  1152. }
  1153. else if (toDistance < 15) {
  1154. this.connectedNode = this.to;
  1155. this.to = this.controlNodes.to;
  1156. return this.controlNodes.to;
  1157. }
  1158. else {
  1159. return null;
  1160. }
  1161. };
  1162. /**
  1163. * this resets the control nodes to their original position.
  1164. * @private
  1165. */
  1166. Edge.prototype._restoreControlNodes = function() {
  1167. if (this.controlNodes.from.selected == true) {
  1168. this.from = this.connectedNode;
  1169. this.connectedNode = null;
  1170. this.controlNodes.from.unselect();
  1171. }
  1172. else if (this.controlNodes.to.selected == true) {
  1173. this.to = this.connectedNode;
  1174. this.connectedNode = null;
  1175. this.controlNodes.to.unselect();
  1176. }
  1177. };
  1178. /**
  1179. * this calculates the position of the control nodes on the edges of the parent nodes.
  1180. *
  1181. * @param ctx
  1182. * @returns {x: *, y: *}
  1183. */
  1184. Edge.prototype.getControlNodeFromPosition = function(ctx) {
  1185. // draw arrow head
  1186. var controlnodeFromPos;
  1187. if (this.options.smoothCurves.enabled == true) {
  1188. controlnodeFromPos = this._findBorderPosition(true, ctx);
  1189. }
  1190. else {
  1191. var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  1192. var dx = (this.to.x - this.from.x);
  1193. var dy = (this.to.y - this.from.y);
  1194. var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
  1195. var fromBorderDist = this.from.distanceToBorder(ctx, angle + Math.PI);
  1196. var fromBorderPoint = (edgeSegmentLength - fromBorderDist) / edgeSegmentLength;
  1197. controlnodeFromPos = {};
  1198. controlnodeFromPos.x = (fromBorderPoint) * this.from.x + (1 - fromBorderPoint) * this.to.x;
  1199. controlnodeFromPos.y = (fromBorderPoint) * this.from.y + (1 - fromBorderPoint) * this.to.y;
  1200. }
  1201. return controlnodeFromPos;
  1202. };
  1203. /**
  1204. * this calculates the position of the control nodes on the edges of the parent nodes.
  1205. *
  1206. * @param ctx
  1207. * @returns {{from: {x: number, y: number}, to: {x: *, y: *}}}
  1208. */
  1209. Edge.prototype.getControlNodeToPosition = function(ctx) {
  1210. // draw arrow head
  1211. var controlnodeFromPos,controlnodeToPos;
  1212. if (this.options.smoothCurves.enabled == true) {
  1213. controlnodeToPos = this._findBorderPosition(false, ctx);
  1214. }
  1215. else {
  1216. var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  1217. var dx = (this.to.x - this.from.x);
  1218. var dy = (this.to.y - this.from.y);
  1219. var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
  1220. var toBorderDist = this.to.distanceToBorder(ctx, angle);
  1221. var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength;
  1222. controlnodeToPos = {};
  1223. controlnodeToPos.x = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x;
  1224. controlnodeToPos.y = (1 - toBorderPoint) * this.from.y + toBorderPoint * this.to.y;
  1225. }
  1226. return controlnodeToPos;
  1227. };
  1228. module.exports = Edge;