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.

527 lines
15 KiB

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