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.

1199 lines
36 KiB

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