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.

475 lines
14 KiB

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