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.

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