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.

1205 lines
36 KiB

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','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. var xVia = null;
  284. var yVia = null;
  285. var factor = this.options.smoothCurves.roundness;
  286. var type = this.options.smoothCurves.type;
  287. var dx = Math.abs(this.from.x - this.to.x);
  288. var dy = Math.abs(this.from.y - this.to.y);
  289. if (type == 'discrete' || type == 'diagonalCross') {
  290. if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) {
  291. if (this.from.y > this.to.y) {
  292. if (this.from.x < this.to.x) {
  293. xVia = this.from.x + factor * dy;
  294. yVia = this.from.y - factor * dy;
  295. }
  296. else if (this.from.x > this.to.x) {
  297. xVia = this.from.x - factor * dy;
  298. yVia = this.from.y - factor * dy;
  299. }
  300. }
  301. else if (this.from.y < this.to.y) {
  302. if (this.from.x < this.to.x) {
  303. xVia = this.from.x + factor * dy;
  304. yVia = this.from.y + factor * dy;
  305. }
  306. else if (this.from.x > this.to.x) {
  307. xVia = this.from.x - factor * dy;
  308. yVia = this.from.y + factor * dy;
  309. }
  310. }
  311. if (type == "discrete") {
  312. xVia = dx < factor * dy ? this.from.x : xVia;
  313. }
  314. }
  315. else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) {
  316. if (this.from.y > this.to.y) {
  317. if (this.from.x < this.to.x) {
  318. xVia = this.from.x + factor * dx;
  319. yVia = this.from.y - factor * dx;
  320. }
  321. else if (this.from.x > this.to.x) {
  322. xVia = this.from.x - factor * dx;
  323. yVia = this.from.y - factor * dx;
  324. }
  325. }
  326. else if (this.from.y < this.to.y) {
  327. if (this.from.x < this.to.x) {
  328. xVia = this.from.x + factor * dx;
  329. yVia = this.from.y + factor * dx;
  330. }
  331. else if (this.from.x > this.to.x) {
  332. xVia = this.from.x - factor * dx;
  333. yVia = this.from.y + factor * dx;
  334. }
  335. }
  336. if (type == "discrete") {
  337. yVia = dy < factor * dx ? this.from.y : yVia;
  338. }
  339. }
  340. }
  341. else if (type == "straightCross") {
  342. if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) { // up - down
  343. xVia = this.from.x;
  344. if (this.from.y < this.to.y) {
  345. yVia = this.to.y - (1-factor) * dy;
  346. }
  347. else {
  348. yVia = this.to.y + (1-factor) * dy;
  349. }
  350. }
  351. else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) { // left - right
  352. if (this.from.x < this.to.x) {
  353. xVia = this.to.x - (1-factor) * dx;
  354. }
  355. else {
  356. xVia = this.to.x + (1-factor) * dx;
  357. }
  358. yVia = this.from.y;
  359. }
  360. }
  361. else if (type == 'horizontal') {
  362. if (this.from.x < this.to.x) {
  363. xVia = this.to.x - (1-factor) * dx;
  364. }
  365. else {
  366. xVia = this.to.x + (1-factor) * dx;
  367. }
  368. yVia = this.from.y;
  369. }
  370. else if (type == 'vertical') {
  371. xVia = this.from.x;
  372. if (this.from.y < this.to.y) {
  373. yVia = this.to.y - (1-factor) * dy;
  374. }
  375. else {
  376. yVia = this.to.y + (1-factor) * dy;
  377. }
  378. }
  379. else { // continuous
  380. if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) {
  381. if (this.from.y > this.to.y) {
  382. if (this.from.x < this.to.x) {
  383. // console.log(1)
  384. xVia = this.from.x + factor * dy;
  385. yVia = this.from.y - factor * dy;
  386. xVia = this.to.x < xVia ? this.to.x : xVia;
  387. }
  388. else if (this.from.x > this.to.x) {
  389. // console.log(2)
  390. xVia = this.from.x - factor * dy;
  391. yVia = this.from.y - factor * dy;
  392. xVia = this.to.x > xVia ? this.to.x :xVia;
  393. }
  394. }
  395. else if (this.from.y < this.to.y) {
  396. if (this.from.x < this.to.x) {
  397. // console.log(3)
  398. xVia = this.from.x + factor * dy;
  399. yVia = this.from.y + factor * dy;
  400. xVia = this.to.x < xVia ? this.to.x : xVia;
  401. }
  402. else if (this.from.x > this.to.x) {
  403. // console.log(4, this.from.x, this.to.x)
  404. xVia = this.from.x - factor * dy;
  405. yVia = this.from.y + factor * dy;
  406. xVia = this.to.x > xVia ? this.to.x : xVia;
  407. }
  408. }
  409. }
  410. else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) {
  411. if (this.from.y > this.to.y) {
  412. if (this.from.x < this.to.x) {
  413. // console.log(5)
  414. xVia = this.from.x + factor * dx;
  415. yVia = this.from.y - factor * dx;
  416. yVia = this.to.y > yVia ? this.to.y : yVia;
  417. }
  418. else if (this.from.x > this.to.x) {
  419. // console.log(6)
  420. xVia = this.from.x - factor * dx;
  421. yVia = this.from.y - factor * dx;
  422. yVia = this.to.y > yVia ? this.to.y : yVia;
  423. }
  424. }
  425. else if (this.from.y < this.to.y) {
  426. if (this.from.x < this.to.x) {
  427. // console.log(7)
  428. xVia = this.from.x + factor * dx;
  429. yVia = this.from.y + factor * dx;
  430. yVia = this.to.y < yVia ? this.to.y : yVia;
  431. }
  432. else if (this.from.x > this.to.x) {
  433. // console.log(8)
  434. xVia = this.from.x - factor * dx;
  435. yVia = this.from.y + factor * dx;
  436. yVia = this.to.y < yVia ? this.to.y : yVia;
  437. }
  438. }
  439. }
  440. }
  441. return {x:xVia, y:yVia};
  442. };
  443. /**
  444. * Draw a line between two nodes
  445. * @param {CanvasRenderingContext2D} ctx
  446. * @private
  447. */
  448. Edge.prototype._line = function (ctx) {
  449. // draw a straight line
  450. ctx.beginPath();
  451. ctx.moveTo(this.from.x, this.from.y);
  452. if (this.options.smoothCurves.enabled == true) {
  453. if (this.options.smoothCurves.dynamic == false) {
  454. var via = this._getViaCoordinates();
  455. if (via.x == null) {
  456. ctx.lineTo(this.to.x, this.to.y);
  457. ctx.stroke();
  458. return null;
  459. }
  460. else {
  461. // this.via.x = via.x;
  462. // this.via.y = via.y;
  463. ctx.quadraticCurveTo(via.x,via.y,this.to.x, this.to.y);
  464. ctx.stroke();
  465. return via;
  466. }
  467. }
  468. else {
  469. ctx.quadraticCurveTo(this.via.x,this.via.y,this.to.x, this.to.y);
  470. ctx.stroke();
  471. return this.via;
  472. }
  473. }
  474. else {
  475. ctx.lineTo(this.to.x, this.to.y);
  476. ctx.stroke();
  477. return null;
  478. }
  479. };
  480. /**
  481. * Draw a line from a node to itself, a circle
  482. * @param {CanvasRenderingContext2D} ctx
  483. * @param {Number} x
  484. * @param {Number} y
  485. * @param {Number} radius
  486. * @private
  487. */
  488. Edge.prototype._circle = function (ctx, x, y, radius) {
  489. // draw a circle
  490. ctx.beginPath();
  491. ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
  492. ctx.stroke();
  493. };
  494. /**
  495. * Draw label with white background and with the middle at (x, y)
  496. * @param {CanvasRenderingContext2D} ctx
  497. * @param {String} text
  498. * @param {Number} x
  499. * @param {Number} y
  500. * @private
  501. */
  502. Edge.prototype._label = function (ctx, text, x, y) {
  503. if (text) {
  504. ctx.font = ((this.from.selected || this.to.selected) ? "bold " : "") +
  505. this.options.fontSize + "px " + this.options.fontFace;
  506. var yLine;
  507. if (this.dirtyLabel == true) {
  508. var lines = String(text).split('\n');
  509. var lineCount = lines.length;
  510. var fontSize = (Number(this.options.fontSize) + 4);
  511. yLine = y + (1 - lineCount) / 2 * fontSize;
  512. var width = ctx.measureText(lines[0]).width;
  513. for (var i = 1; i < lineCount; i++) {
  514. var lineWidth = ctx.measureText(lines[i]).width;
  515. width = lineWidth > width ? lineWidth : width;
  516. }
  517. var height = this.options.fontSize * lineCount;
  518. var left = x - width / 2;
  519. var top = y - height / 2;
  520. // cache
  521. this.labelDimensions = {top:top,left:left,width:width,height:height,yLine:yLine};
  522. }
  523. if (this.options.fontFill !== undefined && this.options.fontFill !== null && this.options.fontFill !== "none") {
  524. ctx.fillStyle = this.options.fontFill;
  525. ctx.fillRect(this.labelDimensions.left,
  526. this.labelDimensions.top,
  527. this.labelDimensions.width,
  528. this.labelDimensions.height);
  529. }
  530. // draw text
  531. ctx.fillStyle = this.options.fontColor || "black";
  532. ctx.textAlign = "center";
  533. ctx.textBaseline = "middle";
  534. yLine = this.labelDimensions.yLine;
  535. for (var i = 0; i < lineCount; i++) {
  536. ctx.fillText(lines[i], x, yLine);
  537. yLine += fontSize;
  538. }
  539. }
  540. };
  541. /**
  542. * Redraw a edge as a dashed line
  543. * Draw this edge in the given canvas
  544. * @author David Jordan
  545. * @date 2012-08-08
  546. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  547. * @param {CanvasRenderingContext2D} ctx
  548. * @private
  549. */
  550. Edge.prototype._drawDashLine = function(ctx) {
  551. // set style
  552. ctx.strokeStyle = this._getColor();
  553. ctx.lineWidth = this._getLineWidth();
  554. var via = null;
  555. // only firefox and chrome support this method, else we use the legacy one.
  556. if (ctx.mozDash !== undefined || ctx.setLineDash !== undefined) {
  557. // configure the dash pattern
  558. var pattern = [0];
  559. if (this.options.dash.length !== undefined && this.options.dash.gap !== undefined) {
  560. pattern = [this.options.dash.length,this.options.dash.gap];
  561. }
  562. else {
  563. pattern = [5,5];
  564. }
  565. // set dash settings for chrome or firefox
  566. if (typeof ctx.setLineDash !== 'undefined') { //Chrome
  567. ctx.setLineDash(pattern);
  568. ctx.lineDashOffset = 0;
  569. } else { //Firefox
  570. ctx.mozDash = pattern;
  571. ctx.mozDashOffset = 0;
  572. }
  573. // draw the line
  574. via = this._line(ctx);
  575. // restore the dash settings.
  576. if (typeof ctx.setLineDash !== 'undefined') { //Chrome
  577. ctx.setLineDash([0]);
  578. ctx.lineDashOffset = 0;
  579. } else { //Firefox
  580. ctx.mozDash = [0];
  581. ctx.mozDashOffset = 0;
  582. }
  583. }
  584. else { // unsupporting smooth lines
  585. // draw dashed line
  586. ctx.beginPath();
  587. ctx.lineCap = 'round';
  588. if (this.options.dash.altLength !== undefined) //If an alt dash value has been set add to the array this value
  589. {
  590. ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
  591. [this.options.dash.length,this.options.dash.gap,this.options.dash.altLength,this.options.dash.gap]);
  592. }
  593. 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
  594. {
  595. ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
  596. [this.options.dash.length,this.options.dash.gap]);
  597. }
  598. else //If all else fails draw a line
  599. {
  600. ctx.moveTo(this.from.x, this.from.y);
  601. ctx.lineTo(this.to.x, this.to.y);
  602. }
  603. ctx.stroke();
  604. }
  605. // draw label
  606. if (this.label) {
  607. var point;
  608. if (this.options.smoothCurves.enabled == true && via != null) {
  609. var midpointX = 0.5*(0.5*(this.from.x + via.x) + 0.5*(this.to.x + via.x));
  610. var midpointY = 0.5*(0.5*(this.from.y + via.y) + 0.5*(this.to.y + via.y));
  611. point = {x:midpointX, y:midpointY};
  612. }
  613. else {
  614. point = this._pointOnLine(0.5);
  615. }
  616. this._label(ctx, this.label, point.x, point.y);
  617. }
  618. };
  619. /**
  620. * Get a point on a line
  621. * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
  622. * @return {Object} point
  623. * @private
  624. */
  625. Edge.prototype._pointOnLine = function (percentage) {
  626. return {
  627. x: (1 - percentage) * this.from.x + percentage * this.to.x,
  628. y: (1 - percentage) * this.from.y + percentage * this.to.y
  629. }
  630. };
  631. /**
  632. * Get a point on a circle
  633. * @param {Number} x
  634. * @param {Number} y
  635. * @param {Number} radius
  636. * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
  637. * @return {Object} point
  638. * @private
  639. */
  640. Edge.prototype._pointOnCircle = function (x, y, radius, percentage) {
  641. var angle = (percentage - 3/8) * 2 * Math.PI;
  642. return {
  643. x: x + radius * Math.cos(angle),
  644. y: y - radius * Math.sin(angle)
  645. }
  646. };
  647. /**
  648. * Redraw a edge as a line with an arrow halfway the line
  649. * Draw this edge in the given canvas
  650. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  651. * @param {CanvasRenderingContext2D} ctx
  652. * @private
  653. */
  654. Edge.prototype._drawArrowCenter = function(ctx) {
  655. var point;
  656. // set style
  657. ctx.strokeStyle = this._getColor();
  658. ctx.fillStyle = ctx.strokeStyle;
  659. ctx.lineWidth = this._getLineWidth();
  660. if (this.from != this.to) {
  661. // draw line
  662. var via = this._line(ctx);
  663. var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  664. var length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor;
  665. // draw an arrow halfway the line
  666. if (this.options.smoothCurves.enabled == true && via != null) {
  667. var midpointX = 0.5*(0.5*(this.from.x + via.x) + 0.5*(this.to.x + via.x));
  668. var midpointY = 0.5*(0.5*(this.from.y + via.y) + 0.5*(this.to.y + via.y));
  669. point = {x:midpointX, y:midpointY};
  670. }
  671. else {
  672. point = this._pointOnLine(0.5);
  673. }
  674. ctx.arrow(point.x, point.y, angle, length);
  675. ctx.fill();
  676. ctx.stroke();
  677. // draw label
  678. if (this.label) {
  679. this._label(ctx, this.label, point.x, point.y);
  680. }
  681. }
  682. else {
  683. // draw circle
  684. var x, y;
  685. var radius = 0.25 * Math.max(100,this.physics.springLength);
  686. var node = this.from;
  687. if (!node.width) {
  688. node.resize(ctx);
  689. }
  690. if (node.width > node.height) {
  691. x = node.x + node.width * 0.5;
  692. y = node.y - radius;
  693. }
  694. else {
  695. x = node.x + radius;
  696. y = node.y - node.height * 0.5;
  697. }
  698. this._circle(ctx, x, y, radius);
  699. // draw all arrows
  700. var angle = 0.2 * Math.PI;
  701. var length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor;
  702. point = this._pointOnCircle(x, y, radius, 0.5);
  703. ctx.arrow(point.x, point.y, angle, length);
  704. ctx.fill();
  705. ctx.stroke();
  706. // draw label
  707. if (this.label) {
  708. point = this._pointOnCircle(x, y, radius, 0.5);
  709. this._label(ctx, this.label, point.x, point.y);
  710. }
  711. }
  712. };
  713. /**
  714. * Redraw a edge as a line with an arrow
  715. * Draw this edge in the given canvas
  716. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  717. * @param {CanvasRenderingContext2D} ctx
  718. * @private
  719. */
  720. Edge.prototype._drawArrow = function(ctx) {
  721. // set style
  722. ctx.strokeStyle = this._getColor();
  723. ctx.fillStyle = ctx.strokeStyle;
  724. ctx.lineWidth = this._getLineWidth();
  725. var angle, length;
  726. //draw a line
  727. if (this.from != this.to) {
  728. angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  729. var dx = (this.to.x - this.from.x);
  730. var dy = (this.to.y - this.from.y);
  731. var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
  732. var fromBorderDist = this.from.distanceToBorder(ctx, angle + Math.PI);
  733. var fromBorderPoint = (edgeSegmentLength - fromBorderDist) / edgeSegmentLength;
  734. var xFrom = (fromBorderPoint) * this.from.x + (1 - fromBorderPoint) * this.to.x;
  735. var yFrom = (fromBorderPoint) * this.from.y + (1 - fromBorderPoint) * this.to.y;
  736. var via;
  737. if (this.options.smoothCurves.dynamic == true && this.options.smoothCurves.enabled == true ) {
  738. via = this.via;
  739. }
  740. else if (this.options.smoothCurves.enabled == true) {
  741. via = this._getViaCoordinates();
  742. }
  743. if (this.options.smoothCurves.enabled == true && via.x != null) {
  744. angle = Math.atan2((this.to.y - via.y), (this.to.x - via.x));
  745. dx = (this.to.x - via.x);
  746. dy = (this.to.y - via.y);
  747. edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
  748. }
  749. var toBorderDist = this.to.distanceToBorder(ctx, angle);
  750. var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength;
  751. var xTo,yTo;
  752. if (this.options.smoothCurves.enabled == true && via.x != null) {
  753. xTo = (1 - toBorderPoint) * via.x + toBorderPoint * this.to.x;
  754. yTo = (1 - toBorderPoint) * via.y + toBorderPoint * this.to.y;
  755. }
  756. else {
  757. xTo = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x;
  758. yTo = (1 - toBorderPoint) * this.from.y + toBorderPoint * this.to.y;
  759. }
  760. ctx.beginPath();
  761. ctx.moveTo(xFrom,yFrom);
  762. if (this.options.smoothCurves.enabled == true && via.x != null) {
  763. ctx.quadraticCurveTo(via.x,via.y,xTo, yTo);
  764. }
  765. else {
  766. ctx.lineTo(xTo, yTo);
  767. }
  768. ctx.stroke();
  769. // draw arrow at the end of the line
  770. length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor;
  771. ctx.arrow(xTo, yTo, angle, length);
  772. ctx.fill();
  773. ctx.stroke();
  774. // draw label
  775. if (this.label) {
  776. var point;
  777. if (this.options.smoothCurves.enabled == true && via != null) {
  778. var midpointX = 0.5*(0.5*(this.from.x + via.x) + 0.5*(this.to.x + via.x));
  779. var midpointY = 0.5*(0.5*(this.from.y + via.y) + 0.5*(this.to.y + via.y));
  780. point = {x:midpointX, y:midpointY};
  781. }
  782. else {
  783. point = this._pointOnLine(0.5);
  784. }
  785. this._label(ctx, this.label, point.x, point.y);
  786. }
  787. }
  788. else {
  789. // draw circle
  790. var node = this.from;
  791. var x, y, arrow;
  792. var radius = 0.25 * Math.max(100,this.physics.springLength);
  793. if (!node.width) {
  794. node.resize(ctx);
  795. }
  796. if (node.width > node.height) {
  797. x = node.x + node.width * 0.5;
  798. y = node.y - radius;
  799. arrow = {
  800. x: x,
  801. y: node.y,
  802. angle: 0.9 * Math.PI
  803. };
  804. }
  805. else {
  806. x = node.x + radius;
  807. y = node.y - node.height * 0.5;
  808. arrow = {
  809. x: node.x,
  810. y: y,
  811. angle: 0.6 * Math.PI
  812. };
  813. }
  814. ctx.beginPath();
  815. // TODO: similarly, for a line without arrows, draw to the border of the nodes instead of the center
  816. ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
  817. ctx.stroke();
  818. // draw all arrows
  819. var length = (10 + 5 * this.options.width) * this.options.arrowScaleFactor;
  820. ctx.arrow(arrow.x, arrow.y, arrow.angle, length);
  821. ctx.fill();
  822. ctx.stroke();
  823. // draw label
  824. if (this.label) {
  825. point = this._pointOnCircle(x, y, radius, 0.5);
  826. this._label(ctx, this.label, point.x, point.y);
  827. }
  828. }
  829. };
  830. /**
  831. * Calculate the distance between a point (x3,y3) and a line segment from
  832. * (x1,y1) to (x2,y2).
  833. * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment
  834. * @param {number} x1
  835. * @param {number} y1
  836. * @param {number} x2
  837. * @param {number} y2
  838. * @param {number} x3
  839. * @param {number} y3
  840. * @private
  841. */
  842. Edge.prototype._getDistanceToEdge = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point
  843. var returnValue = 0;
  844. if (this.from != this.to) {
  845. if (this.options.smoothCurves.enabled == true) {
  846. var xVia, yVia;
  847. if (this.options.smoothCurves.enabled == true && this.options.smoothCurves.dynamic == true) {
  848. xVia = this.via.x;
  849. yVia = this.via.y;
  850. }
  851. else {
  852. var via = this._getViaCoordinates();
  853. xVia = via.x;
  854. yVia = via.y;
  855. }
  856. var minDistance = 1e9;
  857. var distance;
  858. var i,t,x,y, lastX, lastY;
  859. for (i = 0; i < 10; i++) {
  860. t = 0.1*i;
  861. x = Math.pow(1-t,2)*x1 + (2*t*(1 - t))*xVia + Math.pow(t,2)*x2;
  862. y = Math.pow(1-t,2)*y1 + (2*t*(1 - t))*yVia + Math.pow(t,2)*y2;
  863. if (i > 0) {
  864. distance = this._getDistanceToLine(lastX,lastY,x,y, x3,y3);
  865. minDistance = distance < minDistance ? distance : minDistance;
  866. }
  867. lastX = x; lastY = y;
  868. }
  869. returnValue = minDistance;
  870. }
  871. else {
  872. returnValue = this._getDistanceToLine(x1,y1,x2,y2,x3,y3);
  873. }
  874. }
  875. else {
  876. var x, y, dx, dy;
  877. var radius = 0.25 * this.physics.springLength;
  878. var node = this.from;
  879. if (node.width > node.height) {
  880. x = node.x + 0.5 * node.width;
  881. y = node.y - radius;
  882. }
  883. else {
  884. x = node.x + radius;
  885. y = node.y - 0.5 * node.height;
  886. }
  887. dx = x - x3;
  888. dy = y - y3;
  889. returnValue = Math.abs(Math.sqrt(dx*dx + dy*dy) - radius);
  890. }
  891. if (this.labelDimensions.left < x3 &&
  892. this.labelDimensions.left + this.labelDimensions.width > x3 &&
  893. this.labelDimensions.top < y3 &&
  894. this.labelDimensions.top + this.labelDimensions.height > y3) {
  895. return 0;
  896. }
  897. else {
  898. return returnValue;
  899. }
  900. };
  901. Edge.prototype._getDistanceToLine = function(x1,y1,x2,y2,x3,y3) {
  902. var px = x2-x1,
  903. py = y2-y1,
  904. something = px*px + py*py,
  905. u = ((x3 - x1) * px + (y3 - y1) * py) / something;
  906. if (u > 1) {
  907. u = 1;
  908. }
  909. else if (u < 0) {
  910. u = 0;
  911. }
  912. var x = x1 + u * px,
  913. y = y1 + u * py,
  914. dx = x - x3,
  915. dy = y - y3;
  916. //# Note: If the actual distance does not matter,
  917. //# if you only want to compare what this function
  918. //# returns to other results of this function, you
  919. //# can just return the squared distance instead
  920. //# (i.e. remove the sqrt) to gain a little performance
  921. return Math.sqrt(dx*dx + dy*dy);
  922. };
  923. /**
  924. * This allows the zoom level of the network to influence the rendering
  925. *
  926. * @param scale
  927. */
  928. Edge.prototype.setScale = function(scale) {
  929. this.networkScaleInv = 1.0/scale;
  930. };
  931. Edge.prototype.select = function() {
  932. this.selected = true;
  933. };
  934. Edge.prototype.unselect = function() {
  935. this.selected = false;
  936. };
  937. Edge.prototype.positionBezierNode = function() {
  938. if (this.via !== null && this.from !== null && this.to !== null) {
  939. this.via.x = 0.5 * (this.from.x + this.to.x);
  940. this.via.y = 0.5 * (this.from.y + this.to.y);
  941. }
  942. };
  943. /**
  944. * This function draws the control nodes for the manipulator.
  945. * In order to enable this, only set the this.controlNodesEnabled to true.
  946. * @param ctx
  947. */
  948. Edge.prototype._drawControlNodes = function(ctx) {
  949. if (this.controlNodesEnabled == true) {
  950. if (this.controlNodes.from === null && this.controlNodes.to === null) {
  951. var nodeIdFrom = "edgeIdFrom:".concat(this.id);
  952. var nodeIdTo = "edgeIdTo:".concat(this.id);
  953. var constants = {
  954. nodes:{group:'', radius:8},
  955. physics:{damping:0},
  956. clustering: {maxNodeSizeIncrements: 0 ,nodeScaling: {width:0, height: 0, radius:0}}
  957. };
  958. this.controlNodes.from = new Node(
  959. {id:nodeIdFrom,
  960. shape:'dot',
  961. color:{background:'#ff4e00', border:'#3c3c3c', highlight: {background:'#07f968'}}
  962. },{},{},constants);
  963. this.controlNodes.to = new Node(
  964. {id:nodeIdTo,
  965. shape:'dot',
  966. color:{background:'#ff4e00', border:'#3c3c3c', highlight: {background:'#07f968'}}
  967. },{},{},constants);
  968. }
  969. if (this.controlNodes.from.selected == false && this.controlNodes.to.selected == false) {
  970. this.controlNodes.positions = this.getControlNodePositions(ctx);
  971. this.controlNodes.from.x = this.controlNodes.positions.from.x;
  972. this.controlNodes.from.y = this.controlNodes.positions.from.y;
  973. this.controlNodes.to.x = this.controlNodes.positions.to.x;
  974. this.controlNodes.to.y = this.controlNodes.positions.to.y;
  975. }
  976. this.controlNodes.from.draw(ctx);
  977. this.controlNodes.to.draw(ctx);
  978. }
  979. else {
  980. this.controlNodes = {from:null, to:null, positions:{}};
  981. }
  982. };
  983. /**
  984. * Enable control nodes.
  985. * @private
  986. */
  987. Edge.prototype._enableControlNodes = function() {
  988. this.fromBackup = this.from;
  989. this.toBackup = this.to;
  990. this.controlNodesEnabled = true;
  991. };
  992. /**
  993. * disable control nodes and remove from dynamicEdges from old node
  994. * @private
  995. */
  996. Edge.prototype._disableControlNodes = function() {
  997. this.fromId = this.from.id;
  998. this.toId = this.to.id;
  999. if (this.fromId != this.fromBackup.id) { // from was changed, remove edge from old 'from' node dynamic edges
  1000. this.fromBackup.detachEdge(this);
  1001. }
  1002. else if (this.toId != this.toBackup.id) { // to was changed, remove edge from old 'to' node dynamic edges
  1003. this.toBackup.detachEdge(this);
  1004. }
  1005. this.fromBackup = null;
  1006. this.toBackup = null;
  1007. this.controlNodesEnabled = false;
  1008. };
  1009. /**
  1010. * This checks if one of the control nodes is selected and if so, returns the control node object. Else it returns null.
  1011. * @param x
  1012. * @param y
  1013. * @returns {null}
  1014. * @private
  1015. */
  1016. Edge.prototype._getSelectedControlNode = function(x,y) {
  1017. var positions = this.controlNodes.positions;
  1018. var fromDistance = Math.sqrt(Math.pow(x - positions.from.x,2) + Math.pow(y - positions.from.y,2));
  1019. var toDistance = Math.sqrt(Math.pow(x - positions.to.x ,2) + Math.pow(y - positions.to.y ,2));
  1020. if (fromDistance < 15) {
  1021. this.connectedNode = this.from;
  1022. this.from = this.controlNodes.from;
  1023. return this.controlNodes.from;
  1024. }
  1025. else if (toDistance < 15) {
  1026. this.connectedNode = this.to;
  1027. this.to = this.controlNodes.to;
  1028. return this.controlNodes.to;
  1029. }
  1030. else {
  1031. return null;
  1032. }
  1033. };
  1034. /**
  1035. * this resets the control nodes to their original position.
  1036. * @private
  1037. */
  1038. Edge.prototype._restoreControlNodes = function() {
  1039. if (this.controlNodes.from.selected == true) {
  1040. this.from = this.connectedNode;
  1041. this.connectedNode = null;
  1042. this.controlNodes.from.unselect();
  1043. }
  1044. else if (this.controlNodes.to.selected == true) {
  1045. this.to = this.connectedNode;
  1046. this.connectedNode = null;
  1047. this.controlNodes.to.unselect();
  1048. }
  1049. };
  1050. /**
  1051. * this calculates the position of the control nodes on the edges of the parent nodes.
  1052. *
  1053. * @param ctx
  1054. * @returns {{from: {x: number, y: number}, to: {x: *, y: *}}}
  1055. */
  1056. Edge.prototype.getControlNodePositions = function(ctx) {
  1057. var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  1058. var dx = (this.to.x - this.from.x);
  1059. var dy = (this.to.y - this.from.y);
  1060. var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
  1061. var fromBorderDist = this.from.distanceToBorder(ctx, angle + Math.PI);
  1062. var fromBorderPoint = (edgeSegmentLength - fromBorderDist) / edgeSegmentLength;
  1063. var xFrom = (fromBorderPoint) * this.from.x + (1 - fromBorderPoint) * this.to.x;
  1064. var yFrom = (fromBorderPoint) * this.from.y + (1 - fromBorderPoint) * this.to.y;
  1065. var via;
  1066. if (this.options.smoothCurves.dynamic == true && this.options.smoothCurves.enabled == true) {
  1067. via = this.via;
  1068. }
  1069. else if (this.options.smoothCurves.enabled == true) {
  1070. via = this._getViaCoordinates();
  1071. }
  1072. if (this.options.smoothCurves.enabled == true && via.x != null) {
  1073. angle = Math.atan2((this.to.y - via.y), (this.to.x - via.x));
  1074. dx = (this.to.x - via.x);
  1075. dy = (this.to.y - via.y);
  1076. edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
  1077. }
  1078. var toBorderDist = this.to.distanceToBorder(ctx, angle);
  1079. var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength;
  1080. var xTo,yTo;
  1081. if (this.options.smoothCurves.enabled == true && via.x != null) {
  1082. xTo = (1 - toBorderPoint) * via.x + toBorderPoint * this.to.x;
  1083. yTo = (1 - toBorderPoint) * via.y + toBorderPoint * this.to.y;
  1084. }
  1085. else {
  1086. xTo = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x;
  1087. yTo = (1 - toBorderPoint) * this.from.y + toBorderPoint * this.to.y;
  1088. }
  1089. return {from:{x:xFrom,y:yFrom},to:{x:xTo,y:yTo}};
  1090. };
  1091. module.exports = Edge;