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.

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