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.

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