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.

522 lines
14 KiB

9 years ago
9 years ago
9 years ago
  1. let util = require("../../../../../util");
  2. class EdgeBase {
  3. constructor(options, body, labelModule) {
  4. this.body = body;
  5. this.labelModule = labelModule;
  6. this.setOptions(options);
  7. this.colorDirty = true;
  8. this.color = {};
  9. this.selectionWidth = 2;
  10. this.hoverWidth = 1.5;
  11. }
  12. connect() {
  13. this.from = this.body.nodes[this.options.from];
  14. this.to = this.body.nodes[this.options.to];
  15. }
  16. cleanup() {return false}
  17. setOptions(options) {
  18. this.options = options;
  19. this.from = this.body.nodes[this.options.from];
  20. this.to = this.body.nodes[this.options.to];
  21. this.id = this.options.id;
  22. }
  23. /**
  24. * Redraw a edge as a line
  25. * Draw this edge in the given canvas
  26. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  27. * @param {CanvasRenderingContext2D} ctx
  28. * @private
  29. */
  30. drawLine(ctx, selected, hover) {
  31. // set style
  32. ctx.strokeStyle = this.getColor(ctx, selected, hover);
  33. ctx.lineWidth = this.getLineWidth(selected, hover);
  34. let via = undefined;
  35. if (this.options.dashes !== false) {
  36. via = this._drawDashedLine(ctx);
  37. }
  38. else {
  39. via = this._drawLine(ctx);
  40. }
  41. return via;
  42. }
  43. _drawLine(ctx) {
  44. let via = undefined;
  45. if (this.from != this.to) {
  46. // draw line
  47. via = this._line(ctx);
  48. }
  49. else {
  50. let [x,y,radius] = this._getCircleData(ctx);
  51. this._circle(ctx, x, y, radius);
  52. }
  53. return via;
  54. }
  55. _drawDashedLine(ctx) {
  56. let via = undefined;
  57. ctx.lineCap = 'round';
  58. let pattern = [5,5];
  59. if (Array.isArray(this.options.dashes) === true) {
  60. pattern = this.options.dashes;
  61. }
  62. // only firefox and chrome support this method, else we use the legacy one.
  63. if (ctx.setLineDash !== undefined) {
  64. ctx.save();
  65. // set dash settings for chrome or firefox
  66. ctx.setLineDash(pattern);
  67. ctx.lineDashOffset = 0;
  68. // draw the line
  69. if (this.from != this.to) {
  70. // draw line
  71. via = this._line(ctx);
  72. }
  73. else {
  74. let [x,y,radius] = this._getCircleData(ctx);
  75. this._circle(ctx, x, y, radius);
  76. }
  77. // restore the dash settings.
  78. ctx.setLineDash([0]);
  79. ctx.lineDashOffset = 0;
  80. ctx.restore();
  81. }
  82. else { // unsupporting smooth lines
  83. if (this.from != this.to) {
  84. // draw line
  85. ctx.dashedLine(this.from.x, this.from.y, this.to.x, this.to.y, pattern);
  86. }
  87. else {
  88. let [x,y,radius] = this._getCircleData(ctx);
  89. this._circle(ctx, x, y, radius);
  90. }
  91. // draw shadow if enabled
  92. this.enableShadow(ctx);
  93. ctx.stroke();
  94. // disable shadows for other elements.
  95. this.disableShadow(ctx);
  96. }
  97. return via;
  98. }
  99. findBorderPosition(nearNode, ctx, options) {
  100. if (this.from != this.to) {
  101. return this._findBorderPosition(nearNode, ctx, options);
  102. }
  103. else {
  104. return this._findBorderPositionCircle(nearNode, ctx, options);
  105. }
  106. }
  107. findBorderPositions(ctx) {
  108. let from = {};
  109. let to = {};
  110. if (this.from != this.to) {
  111. from = this._findBorderPosition(this.from, ctx);
  112. to = this._findBorderPosition(this.to, ctx);
  113. }
  114. else {
  115. let [x,y,radius] = this._getCircleData(ctx);
  116. from = this._findBorderPositionCircle(this.from, ctx, {x, y, low:0.25, high:0.6, direction:-1});
  117. to = this._findBorderPositionCircle(this.from, ctx, {x, y, low:0.6, high:0.8, direction:1});
  118. }
  119. return {from, to};
  120. }
  121. _getCircleData(ctx) {
  122. let x, y;
  123. let node = this.from;
  124. let radius = this.options.selfReferenceSize;
  125. if (ctx !== undefined) {
  126. if (node.shape.width === undefined) {
  127. node.shape.resize(ctx);
  128. }
  129. }
  130. // get circle coordinates
  131. if (node.shape.width > node.shape.height) {
  132. x = node.x + node.shape.width * 0.5;
  133. y = node.y - radius;
  134. }
  135. else {
  136. x = node.x + radius;
  137. y = node.y - node.shape.height * 0.5;
  138. }
  139. return [x,y,radius];
  140. }
  141. /**
  142. * Get a point on a circle
  143. * @param {Number} x
  144. * @param {Number} y
  145. * @param {Number} radius
  146. * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
  147. * @return {Object} point
  148. * @private
  149. */
  150. _pointOnCircle(x, y, radius, percentage) {
  151. let angle = percentage * 2 * Math.PI;
  152. return {
  153. x: x + radius * Math.cos(angle),
  154. y: y - radius * Math.sin(angle)
  155. }
  156. }
  157. /**
  158. * This function uses binary search to look for the point where the circle crosses the border of the node.
  159. * @param node
  160. * @param ctx
  161. * @param options
  162. * @returns {*}
  163. * @private
  164. */
  165. _findBorderPositionCircle(node, ctx, options) {
  166. let x = options.x;
  167. let y = options.y;
  168. let low = options.low;
  169. let high = options.high;
  170. let direction = options.direction;
  171. let maxIterations = 10;
  172. let iteration = 0;
  173. let radius = this.options.selfReferenceSize;
  174. let pos, angle, distanceToBorder, distanceToPoint, difference;
  175. let threshold = 0.05;
  176. let middle = (low + high) * 0.5;
  177. while (low <= high && iteration < maxIterations) {
  178. middle = (low + high) * 0.5;
  179. pos = this._pointOnCircle(x, y, radius, middle);
  180. angle = Math.atan2((node.y - pos.y), (node.x - pos.x));
  181. distanceToBorder = node.distanceToBorder(ctx, angle);
  182. distanceToPoint = Math.sqrt(Math.pow(pos.x - node.x, 2) + Math.pow(pos.y - node.y, 2));
  183. difference = distanceToBorder - distanceToPoint;
  184. if (Math.abs(difference) < threshold) {
  185. break; // found
  186. }
  187. else if (difference > 0) { // distance to nodes is larger than distance to border --> t needs to be bigger if we're looking at the to node.
  188. if (direction > 0) {
  189. low = middle;
  190. }
  191. else {
  192. high = middle;
  193. }
  194. }
  195. else {
  196. if (direction > 0) {
  197. high = middle;
  198. }
  199. else {
  200. low = middle;
  201. }
  202. }
  203. iteration++;
  204. }
  205. pos.t = middle;
  206. return pos;
  207. }
  208. /**
  209. * Get the line width of the edge. Depends on width and whether one of the
  210. * connected nodes is selected.
  211. * @return {Number} width
  212. * @private
  213. */
  214. getLineWidth(selected, hover) {
  215. if (selected === true) {
  216. return Math.max(this.selectionWidth, 0.3 / this.body.view.scale);
  217. }
  218. else {
  219. if (hover === true) {
  220. return Math.max(this.hoverWidth, 0.3 / this.body.view.scale);
  221. }
  222. else {
  223. return Math.max(this.options.width, 0.3 / this.body.view.scale);
  224. }
  225. }
  226. }
  227. getColor(ctx, selected, hover) {
  228. let colorOptions = this.options.color;
  229. if (colorOptions.inherit !== false) {
  230. // when this is a loop edge, just use the 'from' method
  231. if (colorOptions.inherit === 'both' && this.from.id !== this.to.id) {
  232. let grd = ctx.createLinearGradient(this.from.x, this.from.y, this.to.x, this.to.y);
  233. let fromColor, toColor;
  234. fromColor = this.from.options.color.highlight.border;
  235. toColor = this.to.options.color.highlight.border;
  236. if (this.from.selected === false && this.to.selected === false) {
  237. fromColor = util.overrideOpacity(this.from.options.color.border, this.options.color.opacity);
  238. toColor = util.overrideOpacity(this.to.options.color.border, this.options.color.opacity);
  239. }
  240. else if (this.from.selected === true && this.to.selected === false) {
  241. toColor = this.to.options.color.border;
  242. }
  243. else if (this.from.selected === false && this.to.selected === true) {
  244. fromColor = this.from.options.color.border;
  245. }
  246. grd.addColorStop(0, fromColor);
  247. grd.addColorStop(1, toColor);
  248. // -------------------- this returns -------------------- //
  249. return grd;
  250. }
  251. if (this.colorDirty === true) {
  252. if (colorOptions.inherit === "to") {
  253. this.color.highlight = this.to.options.color.highlight.border;
  254. this.color.hover = this.to.options.color.hover.border;
  255. this.color.color = util.overrideOpacity(this.to.options.color.border, colorOptions.opacity);
  256. }
  257. else { // (this.options.color.inherit.source === "from") {
  258. this.color.highlight = this.from.options.color.highlight.border;
  259. this.color.hover = this.from.options.color.hover.border;
  260. this.color.color = util.overrideOpacity(this.from.options.color.border, colorOptions.opacity);
  261. }
  262. }
  263. }
  264. else if (this.colorDirty === true) {
  265. this.color.highlight = colorOptions.highlight;
  266. this.color.hover = colorOptions.hover;
  267. this.color.color = util.overrideOpacity(colorOptions.color, colorOptions.opacity);
  268. }
  269. // if color inherit is on and gradients are used, the function has already returned by now.
  270. this.colorDirty = false;
  271. if (selected === true) {
  272. return this.color.highlight;
  273. }
  274. else if (hover === true) {
  275. return this.color.hover;
  276. }
  277. else {
  278. return this.color.color;
  279. }
  280. }
  281. /**
  282. * Draw a line from a node to itself, a circle
  283. * @param {CanvasRenderingContext2D} ctx
  284. * @param {Number} x
  285. * @param {Number} y
  286. * @param {Number} radius
  287. * @private
  288. */
  289. _circle(ctx, x, y, radius) {
  290. // draw shadow if enabled
  291. this.enableShadow(ctx);
  292. // draw a circle
  293. ctx.beginPath();
  294. ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
  295. ctx.stroke();
  296. // disable shadows for other elements.
  297. this.disableShadow(ctx);
  298. }
  299. /**
  300. * Calculate the distance between a point (x3,y3) and a line segment from
  301. * (x1,y1) to (x2,y2).
  302. * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment
  303. * @param {number} x1
  304. * @param {number} y1
  305. * @param {number} x2
  306. * @param {number} y2
  307. * @param {number} x3
  308. * @param {number} y3
  309. * @private
  310. */
  311. getDistanceToEdge(x1, y1, x2, y2, x3, y3, via) { // x3,y3 is the point
  312. let returnValue = 0;
  313. if (this.from != this.to) {
  314. returnValue = this._getDistanceToEdge(x1, y1, x2, y2, x3, y3, via)
  315. }
  316. else {
  317. let [x,y,radius] = this._getCircleData();
  318. let dx = x - x3;
  319. let dy = y - y3;
  320. returnValue = Math.abs(Math.sqrt(dx * dx + dy * dy) - radius);
  321. }
  322. if (this.labelModule.size.left < x3 &&
  323. this.labelModule.size.left + this.labelModule.size.width > x3 &&
  324. this.labelModule.size.top < y3 &&
  325. this.labelModule.size.top + this.labelModule.size.height > y3) {
  326. return 0;
  327. }
  328. else {
  329. return returnValue;
  330. }
  331. }
  332. _getDistanceToLine(x1, y1, x2, y2, x3, y3) {
  333. let px = x2 - x1;
  334. let py = y2 - y1;
  335. let something = px * px + py * py;
  336. let u = ((x3 - x1) * px + (y3 - y1) * py) / something;
  337. if (u > 1) {
  338. u = 1;
  339. }
  340. else if (u < 0) {
  341. u = 0;
  342. }
  343. let x = x1 + u * px;
  344. let y = y1 + u * py;
  345. let dx = x - x3;
  346. let dy = y - y3;
  347. //# Note: If the actual distance does not matter,
  348. //# if you only want to compare what this function
  349. //# returns to other results of this function, you
  350. //# can just return the squared distance instead
  351. //# (i.e. remove the sqrt) to gain a little performance
  352. return Math.sqrt(dx * dx + dy * dy);
  353. }
  354. /**
  355. *
  356. * @param ctx
  357. * @param position
  358. * @param viaNode
  359. */
  360. drawArrowHead(ctx, position, viaNode, selected, hover) {
  361. // set style
  362. ctx.strokeStyle = this.getColor(ctx, selected, hover);
  363. ctx.fillStyle = ctx.strokeStyle;
  364. ctx.lineWidth = this.getLineWidth(selected, hover);
  365. // set lets
  366. let angle;
  367. let length;
  368. let arrowPos;
  369. let node1;
  370. let node2;
  371. let guideOffset;
  372. let scaleFactor;
  373. if (position === 'from') {
  374. node1 = this.from;
  375. node2 = this.to;
  376. guideOffset = 0.1;
  377. scaleFactor = this.options.arrows.from.scaleFactor;
  378. }
  379. else if (position === 'to') {
  380. node1 = this.to;
  381. node2 = this.from;
  382. guideOffset = -0.1;
  383. scaleFactor = this.options.arrows.to.scaleFactor;
  384. }
  385. else {
  386. node1 = this.to;
  387. node2 = this.from;
  388. scaleFactor = this.options.arrows.middle.scaleFactor;
  389. }
  390. // if not connected to itself
  391. if (node1 != node2) {
  392. if (position !== 'middle') {
  393. // draw arrow head
  394. if (this.options.smooth.enabled === true) {
  395. arrowPos = this.findBorderPosition(node1, ctx, {via: viaNode});
  396. let guidePos = this.getPoint(Math.max(0.0, Math.min(1.0, arrowPos.t + guideOffset)), viaNode);
  397. angle = Math.atan2((arrowPos.y - guidePos.y), (arrowPos.x - guidePos.x));
  398. }
  399. else {
  400. angle = Math.atan2((node1.y - node2.y), (node1.x - node2.x));
  401. arrowPos = this.findBorderPosition(node1, ctx);
  402. }
  403. }
  404. else {
  405. angle = Math.atan2((node1.y - node2.y), (node1.x - node2.x));
  406. arrowPos = this.getPoint(0.6, viaNode); // this is 0.6 to account for the size of the arrow.
  407. }
  408. // draw arrow at the end of the line
  409. length = (10 + 5 * this.options.width) * scaleFactor;
  410. ctx.arrow(arrowPos.x, arrowPos.y, angle, length);
  411. // draw shadow if enabled
  412. this.enableShadow(ctx);
  413. ctx.fill();
  414. // disable shadows for other elements.
  415. this.disableShadow(ctx);
  416. ctx.stroke();
  417. }
  418. else {
  419. // draw circle
  420. let angle, point;
  421. let [x,y,radius] = this._getCircleData(ctx);
  422. if (position === 'from') {
  423. point = this.findBorderPosition(this.from, ctx, {x, y, low:0.25, high:0.6, direction:-1});
  424. angle = point.t * -2 * Math.PI + 1.5 * Math.PI + 0.1 * Math.PI;
  425. }
  426. else if (position === 'to') {
  427. point = this.findBorderPosition(this.from, ctx, {x, y, low:0.6, high:1.0, direction:1});
  428. angle = point.t * -2 * Math.PI + 1.5 * Math.PI - 1.1 * Math.PI;
  429. }
  430. else {
  431. point = this._pointOnCircle(x, y, radius, 0.175);
  432. angle = 3.9269908169872414; // === 0.175 * -2 * Math.PI + 1.5 * Math.PI + 0.1 * Math.PI;
  433. }
  434. // draw the arrowhead
  435. let length = (10 + 5 * this.options.width) * scaleFactor;
  436. ctx.arrow(point.x, point.y, angle, length);
  437. // draw shadow if enabled
  438. this.enableShadow(ctx);
  439. ctx.fill();
  440. // disable shadows for other elements.
  441. this.disableShadow(ctx);
  442. ctx.stroke();
  443. }
  444. }
  445. enableShadow(ctx) {
  446. if (this.options.shadow.enabled === true) {
  447. ctx.shadowColor = 'rgba(0,0,0,0.5)';
  448. ctx.shadowBlur = this.options.shadow.size;
  449. ctx.shadowOffsetX = this.options.shadow.x;
  450. ctx.shadowOffsetY = this.options.shadow.y;
  451. }
  452. }
  453. disableShadow(ctx) {
  454. if (this.options.shadow.enabled === true) {
  455. ctx.shadowColor = 'rgba(0,0,0,0)';
  456. ctx.shadowBlur = 0;
  457. ctx.shadowOffsetX = 0;
  458. ctx.shadowOffsetY = 0;
  459. }
  460. }
  461. }
  462. export default EdgeBase;