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.

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