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.

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