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.

468 lines
14 KiB

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