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.

719 lines
20 KiB

  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.value = undefined;
  32. this.length = constants.physics.springLength;
  33. this.selected = false;
  34. this.smooth = constants.smoothCurves;
  35. this.from = null; // a node
  36. this.to = null; // a node
  37. this.via = null; // a temp node
  38. // we use this to be able to reconnect the edge to a cluster if its node is put into a cluster
  39. // by storing the original information we can revert to the original connection when the cluser is opened.
  40. this.originalFromId = [];
  41. this.originalToId = [];
  42. this.connected = false;
  43. // Added to support dashed lines
  44. // David Jordan
  45. // 2012-08-08
  46. this.dash = util.extend({}, constants.edges.dash); // contains properties length, gap, altLength
  47. this.color = constants.edges.color;
  48. this.widthFixed = false;
  49. this.lengthFixed = false;
  50. this.setProperties(properties, constants);
  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, constants) {
  58. if (!properties) {
  59. return;
  60. }
  61. if (properties.from !== undefined) {this.fromId = properties.from;}
  62. if (properties.to !== undefined) {this.toId = properties.to;}
  63. if (properties.id !== undefined) {this.id = properties.id;}
  64. if (properties.style !== undefined) {this.style = properties.style;}
  65. if (properties.label !== undefined) {this.label = properties.label;}
  66. if (this.label) {
  67. this.fontSize = constants.edges.fontSize;
  68. this.fontFace = constants.edges.fontFace;
  69. this.fontColor = constants.edges.fontColor;
  70. if (properties.fontColor !== undefined) {this.fontColor = properties.fontColor;}
  71. if (properties.fontSize !== undefined) {this.fontSize = properties.fontSize;}
  72. if (properties.fontFace !== undefined) {this.fontFace = properties.fontFace;}
  73. }
  74. if (properties.title !== undefined) {this.title = properties.title;}
  75. if (properties.width !== undefined) {this.width = properties.width;}
  76. if (properties.value !== undefined) {this.value = properties.value;}
  77. if (properties.length !== undefined) {this.length = properties.length;}
  78. // Added to support dashed lines
  79. // David Jordan
  80. // 2012-08-08
  81. if (properties.dash) {
  82. if (properties.dash.length !== undefined) {this.dash.length = properties.dash.length;}
  83. if (properties.dash.gap !== undefined) {this.dash.gap = properties.dash.gap;}
  84. if (properties.dash.altLength !== undefined) {this.dash.altLength = properties.dash.altLength;}
  85. }
  86. if (properties.color !== undefined) {this.color = properties.color;}
  87. // A node is connected when it has a from and to node.
  88. this.connect();
  89. this.widthFixed = this.widthFixed || (properties.width !== undefined);
  90. this.lengthFixed = this.lengthFixed || (properties.length !== undefined);
  91. // set draw method based on style
  92. switch (this.style) {
  93. case 'line': this.draw = this._drawLine; break;
  94. case 'arrow': this.draw = this._drawArrow; break;
  95. case 'arrow-center': this.draw = this._drawArrowCenter; break;
  96. case 'dash-line': this.draw = this._drawDashLine; break;
  97. default: this.draw = this._drawLine; break;
  98. }
  99. };
  100. /**
  101. * Connect an edge to its nodes
  102. */
  103. Edge.prototype.connect = function () {
  104. this.disconnect();
  105. this.from = this.graph.nodes[this.fromId] || null;
  106. this.to = this.graph.nodes[this.toId] || null;
  107. this.connected = (this.from && this.to);
  108. if (this.connected) {
  109. this.from.attachEdge(this);
  110. this.to.attachEdge(this);
  111. }
  112. else {
  113. if (this.from) {
  114. this.from.detachEdge(this);
  115. }
  116. if (this.to) {
  117. this.to.detachEdge(this);
  118. }
  119. }
  120. };
  121. /**
  122. * Disconnect an edge from its nodes
  123. */
  124. Edge.prototype.disconnect = function () {
  125. if (this.from) {
  126. this.from.detachEdge(this);
  127. this.from = null;
  128. }
  129. if (this.to) {
  130. this.to.detachEdge(this);
  131. this.to = null;
  132. }
  133. this.connected = false;
  134. };
  135. /**
  136. * get the title of this edge.
  137. * @return {string} title The title of the edge, or undefined when no title
  138. * has been set.
  139. */
  140. Edge.prototype.getTitle = function() {
  141. return this.title;
  142. };
  143. /**
  144. * Retrieve the value of the edge. Can be undefined
  145. * @return {Number} value
  146. */
  147. Edge.prototype.getValue = function() {
  148. return this.value;
  149. };
  150. /**
  151. * Adjust the value range of the edge. The edge will adjust it's width
  152. * based on its value.
  153. * @param {Number} min
  154. * @param {Number} max
  155. */
  156. Edge.prototype.setValueRange = function(min, max) {
  157. if (!this.widthFixed && this.value !== undefined) {
  158. var scale = (this.widthMax - this.widthMin) / (max - min);
  159. this.width = (this.value - min) * scale + this.widthMin;
  160. }
  161. };
  162. /**
  163. * Redraw a edge
  164. * Draw this edge in the given canvas
  165. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  166. * @param {CanvasRenderingContext2D} ctx
  167. */
  168. Edge.prototype.draw = function(ctx) {
  169. throw "Method draw not initialized in edge";
  170. };
  171. /**
  172. * Check if this object is overlapping with the provided object
  173. * @param {Object} obj an object with parameters left, top
  174. * @return {boolean} True if location is located on the edge
  175. */
  176. Edge.prototype.isOverlappingWith = function(obj) {
  177. 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. /**
  188. * Redraw a edge as a line
  189. * Draw this edge in the given canvas
  190. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  191. * @param {CanvasRenderingContext2D} ctx
  192. * @private
  193. */
  194. Edge.prototype._drawLine = function(ctx) {
  195. // set style
  196. ctx.strokeStyle = this.color;
  197. ctx.lineWidth = this._getLineWidth();
  198. var point;
  199. if (this.from != this.to+9) {
  200. // draw line
  201. this._line(ctx);
  202. // draw label
  203. if (this.label) {
  204. point = this._pointOnLine(0.5);
  205. this._label(ctx, this.label, point.x, point.y);
  206. }
  207. }
  208. else {
  209. var x, y;
  210. var radius = this.length / 4;
  211. var node = this.from;
  212. if (!node.width) {
  213. node.resize(ctx);
  214. }
  215. if (node.width > node.height) {
  216. x = node.x + node.width / 2;
  217. y = node.y - radius;
  218. }
  219. else {
  220. x = node.x + radius;
  221. y = node.y - node.height / 2;
  222. }
  223. this._circle(ctx, x, y, radius);
  224. point = this._pointOnCircle(x, y, radius, 0.5);
  225. this._label(ctx, this.label, point.x, point.y);
  226. }
  227. };
  228. /**
  229. * Get the line width of the edge. Depends on width and whether one of the
  230. * connected nodes is selected.
  231. * @return {Number} width
  232. * @private
  233. */
  234. Edge.prototype._getLineWidth = function() {
  235. if (this.selected == true) {
  236. return Math.min(this.width * 2, this.widthMax)*this.graphScaleInv;
  237. }
  238. else {
  239. return this.width*this.graphScaleInv;
  240. }
  241. };
  242. /**
  243. * Draw a line between two nodes
  244. * @param {CanvasRenderingContext2D} ctx
  245. * @private
  246. */
  247. Edge.prototype._line = function (ctx) {
  248. // draw a straight line
  249. ctx.beginPath();
  250. ctx.moveTo(this.from.x, this.from.y);
  251. if (this.smooth == true) {
  252. ctx.quadraticCurveTo(this.via.x,this.via.y,this.to.x, this.to.y);
  253. }
  254. else {
  255. ctx.lineTo(this.to.x, this.to.y);
  256. }
  257. ctx.stroke();
  258. };
  259. /**
  260. * Draw a line from a node to itself, a circle
  261. * @param {CanvasRenderingContext2D} ctx
  262. * @param {Number} x
  263. * @param {Number} y
  264. * @param {Number} radius
  265. * @private
  266. */
  267. Edge.prototype._circle = function (ctx, x, y, radius) {
  268. // draw a circle
  269. ctx.beginPath();
  270. ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
  271. ctx.stroke();
  272. };
  273. /**
  274. * Draw label with white background and with the middle at (x, y)
  275. * @param {CanvasRenderingContext2D} ctx
  276. * @param {String} text
  277. * @param {Number} x
  278. * @param {Number} y
  279. * @private
  280. */
  281. Edge.prototype._label = function (ctx, text, x, y) {
  282. if (text) {
  283. // TODO: cache the calculated size
  284. ctx.font = ((this.from.selected || this.to.selected) ? "bold " : "") +
  285. this.fontSize + "px " + this.fontFace;
  286. ctx.fillStyle = 'white';
  287. var width = ctx.measureText(text).width;
  288. var height = this.fontSize;
  289. var left = x - width / 2;
  290. var top = y - height / 2;
  291. ctx.fillRect(left, top, width, height);
  292. // draw text
  293. ctx.fillStyle = this.fontColor || "black";
  294. ctx.textAlign = "left";
  295. ctx.textBaseline = "top";
  296. ctx.fillText(text, left, top);
  297. }
  298. };
  299. /**
  300. * Redraw a edge as a dashed line
  301. * Draw this edge in the given canvas
  302. * @author David Jordan
  303. * @date 2012-08-08
  304. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  305. * @param {CanvasRenderingContext2D} ctx
  306. * @private
  307. */
  308. Edge.prototype._drawDashLine = function(ctx) {
  309. // set style
  310. ctx.strokeStyle = this.color;
  311. ctx.lineWidth = this._getLineWidth();
  312. // only firefox and chrome support this method, else we use the legacy one.
  313. if (ctx.mozDash !== undefined || ctx.setLineDash !== undefined) {
  314. ctx.beginPath();
  315. ctx.moveTo(this.from.x, this.from.y);
  316. // configure the dash pattern
  317. var pattern = [0];
  318. if (this.dash.length !== undefined && this.dash.gap !== undefined) {
  319. pattern = [this.dash.length,this.dash.gap];
  320. }
  321. else {
  322. pattern = [5,5];
  323. }
  324. // set dash settings for chrome or firefox
  325. if (typeof ctx.setLineDash !== 'undefined') { //Chrome
  326. ctx.setLineDash(pattern);
  327. ctx.lineDashOffset = 0;
  328. } else { //Firefox
  329. ctx.mozDash = pattern;
  330. ctx.mozDashOffset = 0;
  331. }
  332. // draw the line
  333. if (this.smooth == true) {
  334. ctx.quadraticCurveTo(this.via.x,this.via.y,this.to.x, this.to.y);
  335. }
  336. else {
  337. ctx.lineTo(this.to.x, this.to.y);
  338. }
  339. ctx.stroke();
  340. // restore the dash settings.
  341. if (typeof ctx.setLineDash !== 'undefined') { //Chrome
  342. ctx.setLineDash([0]);
  343. ctx.lineDashOffset = 0;
  344. } else { //Firefox
  345. ctx.mozDash = [0];
  346. ctx.mozDashOffset = 0;
  347. }
  348. }
  349. else { // unsupporting smooth lines
  350. // draw dashed line
  351. ctx.beginPath();
  352. ctx.lineCap = 'round';
  353. if (this.dash.altLength !== undefined) //If an alt dash value has been set add to the array this value
  354. {
  355. ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
  356. [this.dash.length,this.dash.gap,this.dash.altLength,this.dash.gap]);
  357. }
  358. 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
  359. {
  360. ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
  361. [this.dash.length,this.dash.gap]);
  362. }
  363. else //If all else fails draw a line
  364. {
  365. ctx.moveTo(this.from.x, this.from.y);
  366. ctx.lineTo(this.to.x, this.to.y);
  367. }
  368. ctx.stroke();
  369. }
  370. // draw label
  371. if (this.label) {
  372. var point = this._pointOnLine(0.5);
  373. this._label(ctx, this.label, point.x, point.y);
  374. }
  375. };
  376. /**
  377. * Get a point on a line
  378. * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
  379. * @return {Object} point
  380. * @private
  381. */
  382. Edge.prototype._pointOnLine = function (percentage) {
  383. return {
  384. x: (1 - percentage) * this.from.x + percentage * this.to.x,
  385. y: (1 - percentage) * this.from.y + percentage * this.to.y
  386. }
  387. };
  388. /**
  389. * Get a point on a circle
  390. * @param {Number} x
  391. * @param {Number} y
  392. * @param {Number} radius
  393. * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
  394. * @return {Object} point
  395. * @private
  396. */
  397. Edge.prototype._pointOnCircle = function (x, y, radius, percentage) {
  398. var angle = (percentage - 3/8) * 2 * Math.PI;
  399. return {
  400. x: x + radius * Math.cos(angle),
  401. y: y - radius * Math.sin(angle)
  402. }
  403. };
  404. /**
  405. * Redraw a edge as a line with an arrow halfway the line
  406. * Draw this edge in the given canvas
  407. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  408. * @param {CanvasRenderingContext2D} ctx
  409. * @private
  410. */
  411. Edge.prototype._drawArrowCenter = function(ctx) {
  412. var point;
  413. // set style
  414. ctx.strokeStyle = this.color;
  415. ctx.fillStyle = this.color;
  416. ctx.lineWidth = this._getLineWidth();
  417. if (this.from != this.to) {
  418. // draw line
  419. this._line(ctx);
  420. var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  421. var length = 10 + 5 * this.width; // TODO: make customizable?
  422. // draw an arrow halfway the line
  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. ctx.arrow(point.x, point.y, angle, length);
  432. ctx.fill();
  433. ctx.stroke();
  434. // draw label
  435. if (this.label) {
  436. point = this._pointOnLine(0.5);
  437. this._label(ctx, this.label, point.x, point.y);
  438. }
  439. }
  440. else {
  441. // draw circle
  442. var x, y;
  443. var radius = 0.25 * Math.max(100,this.length);
  444. var node = this.from;
  445. if (!node.width) {
  446. node.resize(ctx);
  447. }
  448. if (node.width > node.height) {
  449. x = node.x + node.width * 0.5;
  450. y = node.y - radius;
  451. }
  452. else {
  453. x = node.x + radius;
  454. y = node.y - node.height * 0.5;
  455. }
  456. this._circle(ctx, x, y, radius);
  457. // draw all arrows
  458. var angle = 0.2 * Math.PI;
  459. var length = 10 + 5 * this.width; // TODO: make customizable?
  460. point = this._pointOnCircle(x, y, radius, 0.5);
  461. ctx.arrow(point.x, point.y, angle, length);
  462. ctx.fill();
  463. ctx.stroke();
  464. // draw label
  465. if (this.label) {
  466. point = this._pointOnCircle(x, y, radius, 0.5);
  467. this._label(ctx, this.label, point.x, point.y);
  468. }
  469. }
  470. };
  471. /**
  472. * Redraw a edge as a line with an arrow
  473. * Draw this edge in the given canvas
  474. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  475. * @param {CanvasRenderingContext2D} ctx
  476. * @private
  477. */
  478. Edge.prototype._drawArrow = function(ctx) {
  479. // set style
  480. ctx.strokeStyle = this.color;
  481. ctx.fillStyle = this.color;
  482. ctx.lineWidth = this._getLineWidth();
  483. var angle, length;
  484. //draw a line
  485. if (this.from != this.to) {
  486. angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  487. var dx = (this.to.x - this.from.x);
  488. var dy = (this.to.y - this.from.y);
  489. var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
  490. var fromBorderDist = this.from.distanceToBorder(ctx, angle + Math.PI);
  491. var fromBorderPoint = (edgeSegmentLength - fromBorderDist) / edgeSegmentLength;
  492. var xFrom = (fromBorderPoint) * this.from.x + (1 - fromBorderPoint) * this.to.x;
  493. var yFrom = (fromBorderPoint) * this.from.y + (1 - fromBorderPoint) * this.to.y;
  494. if (this.smooth == true) {
  495. angle = Math.atan2((this.to.y - this.via.y), (this.to.x - this.via.x));
  496. dx = (this.to.x - this.via.x);
  497. dy = (this.to.y - this.via.y);
  498. edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
  499. }
  500. var toBorderDist = this.to.distanceToBorder(ctx, angle);
  501. var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength;
  502. var xTo = (1 - toBorderPoint) * this.via.x + toBorderPoint * this.to.x;
  503. var yTo = (1 - toBorderPoint) * this.via.y + toBorderPoint * this.to.y;
  504. ctx.beginPath();
  505. ctx.moveTo(xFrom,yFrom);
  506. if (this.smooth == true) {
  507. ctx.quadraticCurveTo(this.via.x,this.via.y,xTo, yTo);
  508. }
  509. else {
  510. ctx.lineTo(xTo, yTo);
  511. }
  512. ctx.stroke();
  513. // draw arrow at the end of the line
  514. length = 10 + 5 * this.width;
  515. ctx.arrow(xTo, yTo, angle, length);
  516. ctx.fill();
  517. ctx.stroke();
  518. // draw label
  519. if (this.label) {
  520. var point = this._pointOnLine(0.5);
  521. this._label(ctx, this.label, point.x, point.y);
  522. }
  523. }
  524. else {
  525. // draw circle
  526. var node = this.from;
  527. var x, y, arrow;
  528. var radius = 0.25 * Math.max(100,this.length);
  529. if (!node.width) {
  530. node.resize(ctx);
  531. }
  532. if (node.width > node.height) {
  533. x = node.x + node.width * 0.5;
  534. y = node.y - radius;
  535. arrow = {
  536. x: x,
  537. y: node.y,
  538. angle: 0.9 * Math.PI
  539. };
  540. }
  541. else {
  542. x = node.x + radius;
  543. y = node.y - node.height * 0.5;
  544. arrow = {
  545. x: node.x,
  546. y: y,
  547. angle: 0.6 * Math.PI
  548. };
  549. }
  550. ctx.beginPath();
  551. // TODO: similarly, for a line without arrows, draw to the border of the nodes instead of the center
  552. ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
  553. ctx.stroke();
  554. // draw all arrows
  555. length = 10 + 5 * this.width; // TODO: make customizable?
  556. ctx.arrow(arrow.x, arrow.y, arrow.angle, length);
  557. ctx.fill();
  558. ctx.stroke();
  559. // draw label
  560. if (this.label) {
  561. point = this._pointOnCircle(x, y, radius, 0.5);
  562. this._label(ctx, this.label, point.x, point.y);
  563. }
  564. }
  565. };
  566. /**
  567. * Calculate the distance between a point (x3,y3) and a line segment from
  568. * (x1,y1) to (x2,y2).
  569. * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment
  570. * @param {number} x1
  571. * @param {number} y1
  572. * @param {number} x2
  573. * @param {number} y2
  574. * @param {number} x3
  575. * @param {number} y3
  576. * @private
  577. */
  578. Edge.prototype._getDistanceToEdge = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point
  579. if (this.smooth == true) {
  580. var minDistance = 1e9;
  581. var i,t,x,y,dx,dy;
  582. for (i = 0; i < 10; i++) {
  583. t = 0.1*i;
  584. x = Math.pow(1-t,2)*x1 + (2*t*(1 - t))*this.via.x + Math.pow(t,2)*x2;
  585. y = Math.pow(1-t,2)*y1 + (2*t*(1 - t))*this.via.y + Math.pow(t,2)*y2;
  586. dx = Math.abs(x3-x);
  587. dy = Math.abs(y3-y);
  588. minDistance = Math.min(minDistance,Math.sqrt(dx*dx + dy*dy));
  589. }
  590. return minDistance
  591. }
  592. else {
  593. var px = x2-x1,
  594. py = y2-y1,
  595. something = px*px + py*py,
  596. u = ((x3 - x1) * px + (y3 - y1) * py) / something;
  597. if (u > 1) {
  598. u = 1;
  599. }
  600. else if (u < 0) {
  601. u = 0;
  602. }
  603. var x = x1 + u * px,
  604. y = y1 + u * py,
  605. dx = x - x3,
  606. dy = y - y3;
  607. //# Note: If the actual distance does not matter,
  608. //# if you only want to compare what this function
  609. //# returns to other results of this function, you
  610. //# can just return the squared distance instead
  611. //# (i.e. remove the sqrt) to gain a little performance
  612. return Math.sqrt(dx*dx + dy*dy);
  613. }
  614. };
  615. /**
  616. * This allows the zoom level of the graph to influence the rendering
  617. *
  618. * @param scale
  619. */
  620. Edge.prototype.setScale = function(scale) {
  621. this.graphScaleInv = 1.0/scale;
  622. };
  623. Edge.prototype.select = function() {
  624. this.selected = true;
  625. };
  626. Edge.prototype.unselect = function() {
  627. this.selected = false;
  628. };
  629. Edge.prototype.positionBezierNode = function() {
  630. if (this.via !== null) {
  631. this.via.x = 0.5 * (this.from.x + this.to.x);
  632. this.via.y = 0.5 * (this.from.y + this.to.y);
  633. }
  634. };