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.

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