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.

454 lines
13 KiB

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