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.

1350 lines
40 KiB

10 years ago
9 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','labelAlignment'
  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);
  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. var yLine = this.labelDimensions.yLine;
  532. ctx.save();
  533. if (this.options.labelAlignment != "horizontal"){
  534. ctx.translate(x, yLine);
  535. this._rotateForLabelAlignment(ctx);
  536. x = 0;
  537. yLine = 0;
  538. }
  539. this._drawLabelRect(ctx);
  540. this._drawLabelText(ctx,x,yLine, lines, lineCount, fontSize);
  541. ctx.restore();
  542. }
  543. };
  544. /**
  545. * Rotates the canvas so the text is most readable
  546. * @param {CanvasRenderingContext2D} ctx
  547. * @private
  548. */
  549. Edge.prototype._rotateForLabelAlignment = function(ctx) {
  550. var dy = this.from.y - this.to.y;
  551. var dx = this.from.x - this.to.x;
  552. var angleInDegrees = Math.atan2(dy, dx);
  553. // rotate so label it is readable
  554. if((angleInDegrees < -1 && dx < 0) || (angleInDegrees > 0 && dx < 0)){
  555. angleInDegrees = angleInDegrees + Math.PI;
  556. }
  557. ctx.rotate(angleInDegrees);
  558. };
  559. /**
  560. * Draws the label rectangle
  561. * @param {CanvasRenderingContext2D} ctx
  562. * @param {String} labelAlignment
  563. * @private
  564. */
  565. Edge.prototype._drawLabelRect = function(ctx) {
  566. if (this.options.fontFill !== undefined && this.options.fontFill !== null && this.options.fontFill !== "none") {
  567. ctx.fillStyle = this.options.fontFill;
  568. var lineMargin = 2;
  569. if (this.options.labelAlignment == 'line-center') {
  570. ctx.fillRect(-this.labelDimensions.width * 0.5, -this.labelDimensions.height * 0.5, this.labelDimensions.width, this.labelDimensions.height);
  571. }
  572. else if (this.options.labelAlignment == 'line-above') {
  573. ctx.fillRect(-this.labelDimensions.width * 0.5, -(this.labelDimensions.height + lineMargin), this.labelDimensions.width, this.labelDimensions.height);
  574. }
  575. else if (this.options.labelAlignment == 'line-below') {
  576. ctx.fillRect(-this.labelDimensions.width * 0.5, lineMargin, this.labelDimensions.width, this.labelDimensions.height);
  577. }
  578. else {
  579. ctx.fillRect(this.labelDimensions.left, this.labelDimensions.top, this.labelDimensions.width, this.labelDimensions.height);
  580. }
  581. }
  582. };
  583. /**
  584. * Draws the label text
  585. * @param {CanvasRenderingContext2D} ctx
  586. * @param {Number} x
  587. * @param {Number} yLine
  588. * @param {Array} lines
  589. * @param {Number} lineCount
  590. * @param {Number} fontSize
  591. * @private
  592. */
  593. Edge.prototype._drawLabelText = function(ctx, x, yLine, lines, lineCount, fontSize) {
  594. // draw text
  595. ctx.fillStyle = this.options.fontColor || "black";
  596. ctx.textAlign = "center";
  597. // check for label alignment
  598. if (this.options.labelAlignment != 'horizontal') {
  599. var lineMargin = 2;
  600. if (this.options.labelAlignment == 'line-above') {
  601. ctx.textBaseline = "alphabetic";
  602. yLine -= 2 * lineMargin; // distance from edge, required because we use alphabetic. Alphabetic has less difference between browsers
  603. }
  604. else if (this.options.labelAlignment == 'line-below') {
  605. ctx.textBaseline = "hanging";
  606. yLine += 2 * lineMargin;// distance from edge, required because we use hanging. Hanging has less difference between browsers
  607. }
  608. else {
  609. ctx.textBaseline = "middle";
  610. }
  611. }
  612. else {
  613. ctx.textBaseline = "middle";
  614. }
  615. // check for strokeWidth
  616. if (this.options.fontStrokeWidth > 0){
  617. ctx.lineWidth = this.options.fontStrokeWidth;
  618. ctx.strokeStyle = this.options.fontStrokeColor;
  619. ctx.lineJoin = 'round';
  620. }
  621. for (var i = 0; i < lineCount; i++) {
  622. if(this.options.fontStrokeWidth > 0){
  623. ctx.strokeText(lines[i], x, yLine);
  624. }
  625. ctx.fillText(lines[i], x, yLine);
  626. yLine += fontSize;
  627. }
  628. };
  629. /**
  630. * Redraw a edge as a dashed line
  631. * Draw this edge in the given canvas
  632. * @author David Jordan
  633. * @date 2012-08-08
  634. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  635. * @param {CanvasRenderingContext2D} ctx
  636. * @private
  637. */
  638. Edge.prototype._drawDashLine = function(ctx) {
  639. // set style
  640. ctx.strokeStyle = this._getColor();
  641. ctx.lineWidth = this._getLineWidth();
  642. var via = null;
  643. // only firefox and chrome support this method, else we use the legacy one.
  644. if (ctx.setLineDash !== undefined) {
  645. ctx.save();
  646. // configure the dash pattern
  647. var pattern = [0];
  648. if (this.options.dash.length !== undefined && this.options.dash.gap !== undefined) {
  649. pattern = [this.options.dash.length,this.options.dash.gap];
  650. }
  651. else {
  652. pattern = [5,5];
  653. }
  654. // set dash settings for chrome or firefox
  655. ctx.setLineDash(pattern);
  656. ctx.lineDashOffset = 0;
  657. // draw the line
  658. via = this._line(ctx);
  659. // restore the dash settings.
  660. ctx.setLineDash([0]);
  661. ctx.lineDashOffset = 0;
  662. ctx.restore();
  663. }
  664. else { // unsupporting smooth lines
  665. // draw dashed line
  666. ctx.beginPath();
  667. ctx.lineCap = 'round';
  668. if (this.options.dash.altLength !== undefined) //If an alt dash value has been set add to the array this value
  669. {
  670. ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
  671. [this.options.dash.length,this.options.dash.gap,this.options.dash.altLength,this.options.dash.gap]);
  672. }
  673. 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
  674. {
  675. ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
  676. [this.options.dash.length,this.options.dash.gap]);
  677. }
  678. else //If all else fails draw a line
  679. {
  680. ctx.moveTo(this.from.x, this.from.y);
  681. ctx.lineTo(this.to.x, this.to.y);
  682. }
  683. ctx.stroke();
  684. }
  685. // draw label
  686. if (this.label) {
  687. var point;
  688. if (this.options.smoothCurves.enabled == true && via != null) {
  689. var midpointX = 0.5*(0.5*(this.from.x + via.x) + 0.5*(this.to.x + via.x));
  690. var midpointY = 0.5*(0.5*(this.from.y + via.y) + 0.5*(this.to.y + via.y));
  691. point = {x:midpointX, y:midpointY};
  692. }
  693. else {
  694. point = this._pointOnLine(0.5);
  695. }
  696. this._label(ctx, this.label, point.x, point.y);
  697. }
  698. };
  699. /**
  700. * Get a point on a line
  701. * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
  702. * @return {Object} point
  703. * @private
  704. */
  705. Edge.prototype._pointOnLine = function (percentage) {
  706. return {
  707. x: (1 - percentage) * this.from.x + percentage * this.to.x,
  708. y: (1 - percentage) * this.from.y + percentage * this.to.y
  709. }
  710. };
  711. /**
  712. * Get a point on a circle
  713. * @param {Number} x
  714. * @param {Number} y
  715. * @param {Number} radius
  716. * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
  717. * @return {Object} point
  718. * @private
  719. */
  720. Edge.prototype._pointOnCircle = function (x, y, radius, percentage) {
  721. var angle = (percentage - 3/8) * 2 * Math.PI;
  722. return {
  723. x: x + radius * Math.cos(angle),
  724. y: y - radius * Math.sin(angle)
  725. }
  726. };
  727. /**
  728. * Redraw a edge as a line with an arrow halfway the line
  729. * Draw this edge in the given canvas
  730. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  731. * @param {CanvasRenderingContext2D} ctx
  732. * @private
  733. */
  734. Edge.prototype._drawArrowCenter = function(ctx) {
  735. var point;
  736. // set style
  737. ctx.strokeStyle = this._getColor();
  738. ctx.fillStyle = ctx.strokeStyle;
  739. ctx.lineWidth = this._getLineWidth();
  740. if (this.from != this.to) {
  741. // draw line
  742. var via = this._line(ctx);
  743. var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  744. var length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor;
  745. // draw an arrow halfway the line
  746. if (this.options.smoothCurves.enabled == true && via != null) {
  747. var midpointX = 0.5*(0.5*(this.from.x + via.x) + 0.5*(this.to.x + via.x));
  748. var midpointY = 0.5*(0.5*(this.from.y + via.y) + 0.5*(this.to.y + via.y));
  749. point = {x:midpointX, y:midpointY};
  750. }
  751. else {
  752. point = this._pointOnLine(0.5);
  753. }
  754. ctx.arrow(point.x, point.y, angle, length);
  755. ctx.fill();
  756. ctx.stroke();
  757. // draw label
  758. if (this.label) {
  759. this._label(ctx, this.label, point.x, point.y);
  760. }
  761. }
  762. else {
  763. // draw circle
  764. var x, y;
  765. var radius = 0.25 * Math.max(100,this.physics.springLength);
  766. var node = this.from;
  767. if (!node.width) {
  768. node.resize(ctx);
  769. }
  770. if (node.width > node.height) {
  771. x = node.x + node.width * 0.5;
  772. y = node.y - radius;
  773. }
  774. else {
  775. x = node.x + radius;
  776. y = node.y - node.height * 0.5;
  777. }
  778. this._circle(ctx, x, y, radius);
  779. // draw all arrows
  780. var angle = 0.2 * Math.PI;
  781. var length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor;
  782. point = this._pointOnCircle(x, y, radius, 0.5);
  783. ctx.arrow(point.x, point.y, angle, length);
  784. ctx.fill();
  785. ctx.stroke();
  786. // draw label
  787. if (this.label) {
  788. point = this._pointOnCircle(x, y, radius, 0.5);
  789. this._label(ctx, this.label, point.x, point.y);
  790. }
  791. }
  792. };
  793. Edge.prototype._pointOnBezier = function(t) {
  794. var via = this._getViaCoordinates();
  795. var x = Math.pow(1-t,2)*this.from.x + (2*t*(1 - t))*via.x + Math.pow(t,2)*this.to.x;
  796. var y = Math.pow(1-t,2)*this.from.y + (2*t*(1 - t))*via.y + Math.pow(t,2)*this.to.y;
  797. return {x:x,y:y};
  798. }
  799. /**
  800. * This function uses binary search to look for the point where the bezier curve crosses the border of the node.
  801. *
  802. * @param from
  803. * @param ctx
  804. * @returns {*}
  805. * @private
  806. */
  807. Edge.prototype._findBorderPosition = function(from,ctx) {
  808. var maxIterations = 10;
  809. var iteration = 0;
  810. var low = 0;
  811. var high = 1;
  812. var pos,angle,distanceToBorder, distanceToNodes, difference;
  813. var threshold = 0.2;
  814. var node = this.to;
  815. if (from == true) {
  816. node = this.from;
  817. }
  818. while (low <= high && iteration < maxIterations) {
  819. var middle = (low + high) * 0.5;
  820. pos = this._pointOnBezier(middle);
  821. angle = Math.atan2((node.y - pos.y), (node.x - pos.x));
  822. distanceToBorder = node.distanceToBorder(ctx,angle);
  823. distanceToNodes = Math.sqrt(Math.pow(pos.x-node.x,2) + Math.pow(pos.y-node.y,2));
  824. difference = distanceToBorder - distanceToNodes;
  825. if (Math.abs(difference) < threshold) {
  826. break; // found
  827. }
  828. 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.
  829. if (from == false) {
  830. low = middle;
  831. }
  832. else {
  833. high = middle;
  834. }
  835. }
  836. else {
  837. if (from == false) {
  838. high = middle;
  839. }
  840. else {
  841. low = middle;
  842. }
  843. }
  844. iteration++;
  845. }
  846. pos.t = middle;
  847. return pos;
  848. };
  849. /**
  850. * Redraw a edge as a line with an arrow
  851. * Draw this edge in the given canvas
  852. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  853. * @param {CanvasRenderingContext2D} ctx
  854. * @private
  855. */
  856. Edge.prototype._drawArrow = function(ctx) {
  857. // set style
  858. ctx.strokeStyle = this._getColor();
  859. ctx.fillStyle = ctx.strokeStyle;
  860. ctx.lineWidth = this._getLineWidth();
  861. // set vars
  862. var angle, length, arrowPos;
  863. // if not connected to itself
  864. if (this.from != this.to) {
  865. // draw line
  866. this._line(ctx);
  867. // draw arrow head
  868. if (this.options.smoothCurves.enabled == true) {
  869. var via = this._getViaCoordinates();
  870. arrowPos = this._findBorderPosition(false, ctx);
  871. var guidePos = this._pointOnBezier(Math.max(0.0, arrowPos.t - 0.1))
  872. angle = Math.atan2((arrowPos.y - guidePos.y), (arrowPos.x - guidePos.x));
  873. }
  874. else {
  875. angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  876. var dx = (this.to.x - this.from.x);
  877. var dy = (this.to.y - this.from.y);
  878. var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
  879. var toBorderDist = this.to.distanceToBorder(ctx, angle);
  880. var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength;
  881. arrowPos = {};
  882. arrowPos.x = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x;
  883. arrowPos.y = (1 - toBorderPoint) * this.from.y + toBorderPoint * this.to.y;
  884. }
  885. // draw arrow at the end of the line
  886. length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor;
  887. ctx.arrow(arrowPos.x,arrowPos.y, angle, length);
  888. ctx.fill();
  889. ctx.stroke();
  890. // draw label
  891. if (this.label) {
  892. var point;
  893. if (this.options.smoothCurves.enabled == true && via != null) {
  894. point = this._pointOnBezier(0.5);
  895. }
  896. else {
  897. point = this._pointOnLine(0.5);
  898. }
  899. this._label(ctx, this.label, point.x, point.y);
  900. }
  901. }
  902. else {
  903. // draw circle
  904. var node = this.from;
  905. var x, y, arrow;
  906. var radius = 0.25 * Math.max(100,this.physics.springLength);
  907. if (!node.width) {
  908. node.resize(ctx);
  909. }
  910. if (node.width > node.height) {
  911. x = node.x + node.width * 0.5;
  912. y = node.y - radius;
  913. arrow = {
  914. x: x,
  915. y: node.y,
  916. angle: 0.9 * Math.PI
  917. };
  918. }
  919. else {
  920. x = node.x + radius;
  921. y = node.y - node.height * 0.5;
  922. arrow = {
  923. x: node.x,
  924. y: y,
  925. angle: 0.6 * Math.PI
  926. };
  927. }
  928. ctx.beginPath();
  929. // TODO: similarly, for a line without arrows, draw to the border of the nodes instead of the center
  930. ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
  931. ctx.stroke();
  932. // draw all arrows
  933. var length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor;
  934. ctx.arrow(arrow.x, arrow.y, arrow.angle, length);
  935. ctx.fill();
  936. ctx.stroke();
  937. // draw label
  938. if (this.label) {
  939. point = this._pointOnCircle(x, y, radius, 0.5);
  940. this._label(ctx, this.label, point.x, point.y);
  941. }
  942. }
  943. };
  944. /**
  945. * Calculate the distance between a point (x3,y3) and a line segment from
  946. * (x1,y1) to (x2,y2).
  947. * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment
  948. * @param {number} x1
  949. * @param {number} y1
  950. * @param {number} x2
  951. * @param {number} y2
  952. * @param {number} x3
  953. * @param {number} y3
  954. * @private
  955. */
  956. Edge.prototype._getDistanceToEdge = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point
  957. var returnValue = 0;
  958. if (this.from != this.to) {
  959. if (this.options.smoothCurves.enabled == true) {
  960. var xVia, yVia;
  961. if (this.options.smoothCurves.enabled == true && this.options.smoothCurves.dynamic == true) {
  962. xVia = this.via.x;
  963. yVia = this.via.y;
  964. }
  965. else {
  966. var via = this._getViaCoordinates();
  967. xVia = via.x;
  968. yVia = via.y;
  969. }
  970. var minDistance = 1e9;
  971. var distance;
  972. var i,t,x,y, lastX, lastY;
  973. for (i = 0; i < 10; i++) {
  974. t = 0.1*i;
  975. x = Math.pow(1-t,2)*x1 + (2*t*(1 - t))*xVia + Math.pow(t,2)*x2;
  976. y = Math.pow(1-t,2)*y1 + (2*t*(1 - t))*yVia + Math.pow(t,2)*y2;
  977. if (i > 0) {
  978. distance = this._getDistanceToLine(lastX,lastY,x,y, x3,y3);
  979. minDistance = distance < minDistance ? distance : minDistance;
  980. }
  981. lastX = x; lastY = y;
  982. }
  983. returnValue = minDistance;
  984. }
  985. else {
  986. returnValue = this._getDistanceToLine(x1,y1,x2,y2,x3,y3);
  987. }
  988. }
  989. else {
  990. var x, y, dx, dy;
  991. var radius = 0.25 * this.physics.springLength;
  992. var node = this.from;
  993. if (node.width > node.height) {
  994. x = node.x + 0.5 * node.width;
  995. y = node.y - radius;
  996. }
  997. else {
  998. x = node.x + radius;
  999. y = node.y - 0.5 * node.height;
  1000. }
  1001. dx = x - x3;
  1002. dy = y - y3;
  1003. returnValue = Math.abs(Math.sqrt(dx*dx + dy*dy) - radius);
  1004. }
  1005. if (this.labelDimensions.left < x3 &&
  1006. this.labelDimensions.left + this.labelDimensions.width > x3 &&
  1007. this.labelDimensions.top < y3 &&
  1008. this.labelDimensions.top + this.labelDimensions.height > y3) {
  1009. return 0;
  1010. }
  1011. else {
  1012. return returnValue;
  1013. }
  1014. };
  1015. Edge.prototype._getDistanceToLine = function(x1,y1,x2,y2,x3,y3) {
  1016. var px = x2-x1,
  1017. py = y2-y1,
  1018. something = px*px + py*py,
  1019. u = ((x3 - x1) * px + (y3 - y1) * py) / something;
  1020. if (u > 1) {
  1021. u = 1;
  1022. }
  1023. else if (u < 0) {
  1024. u = 0;
  1025. }
  1026. var x = x1 + u * px,
  1027. y = y1 + u * py,
  1028. dx = x - x3,
  1029. dy = y - y3;
  1030. //# Note: If the actual distance does not matter,
  1031. //# if you only want to compare what this function
  1032. //# returns to other results of this function, you
  1033. //# can just return the squared distance instead
  1034. //# (i.e. remove the sqrt) to gain a little performance
  1035. return Math.sqrt(dx*dx + dy*dy);
  1036. };
  1037. /**
  1038. * This allows the zoom level of the network to influence the rendering
  1039. *
  1040. * @param scale
  1041. */
  1042. Edge.prototype.setScale = function(scale) {
  1043. this.networkScaleInv = 1.0/scale;
  1044. };
  1045. Edge.prototype.select = function() {
  1046. this.selected = true;
  1047. };
  1048. Edge.prototype.unselect = function() {
  1049. this.selected = false;
  1050. };
  1051. Edge.prototype.positionBezierNode = function() {
  1052. if (this.via !== null && this.from !== null && this.to !== null) {
  1053. this.via.x = 0.5 * (this.from.x + this.to.x);
  1054. this.via.y = 0.5 * (this.from.y + this.to.y);
  1055. }
  1056. else if (this.via !== null) {
  1057. this.via.x = 0;
  1058. this.via.y = 0;
  1059. }
  1060. };
  1061. /**
  1062. * This function draws the control nodes for the manipulator.
  1063. * In order to enable this, only set the this.controlNodesEnabled to true.
  1064. * @param ctx
  1065. */
  1066. Edge.prototype._drawControlNodes = function(ctx) {
  1067. if (this.controlNodesEnabled == true) {
  1068. if (this.controlNodes.from === null && this.controlNodes.to === null) {
  1069. var nodeIdFrom = "edgeIdFrom:".concat(this.id);
  1070. var nodeIdTo = "edgeIdTo:".concat(this.id);
  1071. var constants = {
  1072. nodes:{group:'', radius:7, borderWidth:2, borderWidthSelected: 2},
  1073. physics:{damping:0},
  1074. clustering: {maxNodeSizeIncrements: 0 ,nodeScaling: {width:0, height: 0, radius:0}}
  1075. };
  1076. this.controlNodes.from = new Node(
  1077. {id:nodeIdFrom,
  1078. shape:'dot',
  1079. color:{background:'#ff0000', border:'#3c3c3c', highlight: {background:'#07f968'}}
  1080. },{},{},constants);
  1081. this.controlNodes.to = new Node(
  1082. {id:nodeIdTo,
  1083. shape:'dot',
  1084. color:{background:'#ff0000', border:'#3c3c3c', highlight: {background:'#07f968'}}
  1085. },{},{},constants);
  1086. }
  1087. this.controlNodes.positions = {};
  1088. if (this.controlNodes.from.selected == false) {
  1089. this.controlNodes.positions.from = this.getControlNodeFromPosition(ctx);
  1090. this.controlNodes.from.x = this.controlNodes.positions.from.x;
  1091. this.controlNodes.from.y = this.controlNodes.positions.from.y;
  1092. }
  1093. if (this.controlNodes.to.selected == false) {
  1094. this.controlNodes.positions.to = this.getControlNodeToPosition(ctx);
  1095. this.controlNodes.to.x = this.controlNodes.positions.to.x;
  1096. this.controlNodes.to.y = this.controlNodes.positions.to.y;
  1097. }
  1098. this.controlNodes.from.draw(ctx);
  1099. this.controlNodes.to.draw(ctx);
  1100. }
  1101. else {
  1102. this.controlNodes = {from:null, to:null, positions:{}};
  1103. }
  1104. };
  1105. /**
  1106. * Enable control nodes.
  1107. * @private
  1108. */
  1109. Edge.prototype._enableControlNodes = function() {
  1110. this.fromBackup = this.from;
  1111. this.toBackup = this.to;
  1112. this.controlNodesEnabled = true;
  1113. };
  1114. /**
  1115. * disable control nodes and remove from dynamicEdges from old node
  1116. * @private
  1117. */
  1118. Edge.prototype._disableControlNodes = function() {
  1119. this.fromId = this.from.id;
  1120. this.toId = this.to.id;
  1121. if (this.fromId != this.fromBackup.id) { // from was changed, remove edge from old 'from' node dynamic edges
  1122. this.fromBackup.detachEdge(this);
  1123. }
  1124. else if (this.toId != this.toBackup.id) { // to was changed, remove edge from old 'to' node dynamic edges
  1125. this.toBackup.detachEdge(this);
  1126. }
  1127. this.fromBackup = null;
  1128. this.toBackup = null;
  1129. this.controlNodesEnabled = false;
  1130. };
  1131. /**
  1132. * This checks if one of the control nodes is selected and if so, returns the control node object. Else it returns null.
  1133. * @param x
  1134. * @param y
  1135. * @returns {null}
  1136. * @private
  1137. */
  1138. Edge.prototype._getSelectedControlNode = function(x,y) {
  1139. var positions = this.controlNodes.positions;
  1140. var fromDistance = Math.sqrt(Math.pow(x - positions.from.x,2) + Math.pow(y - positions.from.y,2));
  1141. var toDistance = Math.sqrt(Math.pow(x - positions.to.x ,2) + Math.pow(y - positions.to.y ,2));
  1142. if (fromDistance < 15) {
  1143. this.connectedNode = this.from;
  1144. this.from = this.controlNodes.from;
  1145. return this.controlNodes.from;
  1146. }
  1147. else if (toDistance < 15) {
  1148. this.connectedNode = this.to;
  1149. this.to = this.controlNodes.to;
  1150. return this.controlNodes.to;
  1151. }
  1152. else {
  1153. return null;
  1154. }
  1155. };
  1156. /**
  1157. * this resets the control nodes to their original position.
  1158. * @private
  1159. */
  1160. Edge.prototype._restoreControlNodes = function() {
  1161. if (this.controlNodes.from.selected == true) {
  1162. this.from = this.connectedNode;
  1163. this.connectedNode = null;
  1164. this.controlNodes.from.unselect();
  1165. }
  1166. else if (this.controlNodes.to.selected == true) {
  1167. this.to = this.connectedNode;
  1168. this.connectedNode = null;
  1169. this.controlNodes.to.unselect();
  1170. }
  1171. };
  1172. /**
  1173. * this calculates the position of the control nodes on the edges of the parent nodes.
  1174. *
  1175. * @param ctx
  1176. * @returns {x: *, y: *}
  1177. */
  1178. Edge.prototype.getControlNodeFromPosition = function(ctx) {
  1179. // draw arrow head
  1180. var controlnodeFromPos;
  1181. if (this.options.smoothCurves.enabled == true) {
  1182. controlnodeFromPos = this._findBorderPosition(true, ctx);
  1183. }
  1184. else {
  1185. var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  1186. var dx = (this.to.x - this.from.x);
  1187. var dy = (this.to.y - this.from.y);
  1188. var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
  1189. var fromBorderDist = this.from.distanceToBorder(ctx, angle + Math.PI);
  1190. var fromBorderPoint = (edgeSegmentLength - fromBorderDist) / edgeSegmentLength;
  1191. controlnodeFromPos = {};
  1192. controlnodeFromPos.x = (fromBorderPoint) * this.from.x + (1 - fromBorderPoint) * this.to.x;
  1193. controlnodeFromPos.y = (fromBorderPoint) * this.from.y + (1 - fromBorderPoint) * this.to.y;
  1194. }
  1195. return controlnodeFromPos;
  1196. };
  1197. /**
  1198. * this calculates the position of the control nodes on the edges of the parent nodes.
  1199. *
  1200. * @param ctx
  1201. * @returns {{from: {x: number, y: number}, to: {x: *, y: *}}}
  1202. */
  1203. Edge.prototype.getControlNodeToPosition = function(ctx) {
  1204. // draw arrow head
  1205. var controlnodeFromPos,controlnodeToPos;
  1206. if (this.options.smoothCurves.enabled == true) {
  1207. controlnodeToPos = this._findBorderPosition(false, ctx);
  1208. }
  1209. else {
  1210. var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  1211. var dx = (this.to.x - this.from.x);
  1212. var dy = (this.to.y - this.from.y);
  1213. var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
  1214. var toBorderDist = this.to.distanceToBorder(ctx, angle);
  1215. var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength;
  1216. controlnodeToPos = {};
  1217. controlnodeToPos.x = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x;
  1218. controlnodeToPos.y = (1 - toBorderPoint) * this.from.y + toBorderPoint * this.to.y;
  1219. }
  1220. return controlnodeToPos;
  1221. };
  1222. module.exports = Edge;