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.

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