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.

964 lines
28 KiB

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