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.

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