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.

523 lines
15 KiB

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