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.

1252 lines
37 KiB

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