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
  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() {
  20. return false;
  21. }
  22. setOptions(options) {
  23. this.options = options;
  24. this.from = this.body.nodes[this.options.from];
  25. this.to = this.body.nodes[this.options.to];
  26. this.id = this.options.id;
  27. }
  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. *
  33. * @param {CanvasRenderingContext2D} ctx
  34. * @param {Array} values
  35. * @param {boolean} selected
  36. * @param {boolean} hover
  37. * @param {vis.Node} viaNode
  38. * @private
  39. */
  40. drawLine(ctx, values, selected, hover, viaNode) {
  41. // set style
  42. ctx.strokeStyle = this.getColor(ctx, values, selected, hover);
  43. ctx.lineWidth = values.width;
  44. if (values.dashes !== false) {
  45. this._drawDashedLine(ctx, values, viaNode);
  46. }
  47. else {
  48. this._drawLine(ctx, values, viaNode);
  49. }
  50. }
  51. _drawLine(ctx, values, viaNode, fromPoint, toPoint) {
  52. if (this.from != this.to) {
  53. // draw line
  54. this._line(ctx, values, viaNode, fromPoint, toPoint);
  55. }
  56. else {
  57. let [x,y,radius] = this._getCircleData(ctx);
  58. this._circle(ctx, values, x, y, radius);
  59. }
  60. }
  61. _drawDashedLine(ctx, values, viaNode, fromPoint, toPoint) { // eslint-disable-line no-unused-vars
  62. ctx.lineCap = 'round';
  63. let pattern = [5,5];
  64. if (Array.isArray(values.dashes) === true) {
  65. pattern = values.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. this._line(ctx, values, viaNode);
  77. }
  78. else {
  79. let [x,y,radius] = this._getCircleData(ctx);
  80. this._circle(ctx, values, 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, values, x, y, radius);
  95. }
  96. // draw shadow if enabled
  97. this.enableShadow(ctx, values);
  98. ctx.stroke();
  99. // disable shadows for other elements.
  100. this.disableShadow(ctx, values);
  101. }
  102. }
  103. findBorderPosition(nearNode, ctx, options) {
  104. if (this.from != this.to) {
  105. return this._findBorderPosition(nearNode, ctx, options);
  106. }
  107. else {
  108. return this._findBorderPositionCircle(nearNode, ctx, options);
  109. }
  110. }
  111. findBorderPositions(ctx) {
  112. let from = {};
  113. let to = {};
  114. if (this.from != this.to) {
  115. from = this._findBorderPosition(this.from, ctx);
  116. to = this._findBorderPosition(this.to, ctx);
  117. }
  118. else {
  119. let [x,y] = this._getCircleData(ctx).slice(0, 2);
  120. from = this._findBorderPositionCircle(this.from, ctx, {x, y, low:0.25, high:0.6, direction:-1});
  121. to = this._findBorderPositionCircle(this.from, ctx, {x, y, low:0.6, high:0.8, direction:1});
  122. }
  123. return {from, to};
  124. }
  125. _getCircleData(ctx) {
  126. let x, y;
  127. let node = this.from;
  128. let radius = this.options.selfReferenceSize;
  129. if (ctx !== undefined) {
  130. if (node.shape.width === undefined) {
  131. node.shape.resize(ctx);
  132. }
  133. }
  134. // get circle coordinates
  135. if (node.shape.width > node.shape.height) {
  136. x = node.x + node.shape.width * 0.5;
  137. y = node.y - radius;
  138. }
  139. else {
  140. x = node.x + radius;
  141. y = node.y - node.shape.height * 0.5;
  142. }
  143. return [x,y,radius];
  144. }
  145. /**
  146. * Get a point on a circle
  147. * @param {Number} x
  148. * @param {Number} y
  149. * @param {Number} radius
  150. * @param {Number} percentage - Value between 0 (line start) and 1 (line end)
  151. * @return {Object} point
  152. * @private
  153. */
  154. _pointOnCircle(x, y, radius, percentage) {
  155. let angle = percentage * 2 * Math.PI;
  156. return {
  157. x: x + radius * Math.cos(angle),
  158. y: y - radius * Math.sin(angle)
  159. }
  160. }
  161. /**
  162. * This function uses binary search to look for the point where the circle crosses the border of the node.
  163. * @param {vis.Node} node
  164. * @param {CanvasRenderingContext2D} ctx
  165. * @param {Object} options
  166. * @returns {*}
  167. * @private
  168. */
  169. _findBorderPositionCircle(node, ctx, options) {
  170. let x = options.x;
  171. let y = options.y;
  172. let low = options.low;
  173. let high = options.high;
  174. let direction = options.direction;
  175. let maxIterations = 10;
  176. let iteration = 0;
  177. let radius = this.options.selfReferenceSize;
  178. let pos, angle, distanceToBorder, distanceToPoint, difference;
  179. let threshold = 0.05;
  180. let middle = (low + high) * 0.5;
  181. while (low <= high && iteration < maxIterations) {
  182. middle = (low + high) * 0.5;
  183. pos = this._pointOnCircle(x, y, radius, middle);
  184. angle = Math.atan2((node.y - pos.y), (node.x - pos.x));
  185. distanceToBorder = node.distanceToBorder(ctx, angle);
  186. distanceToPoint = Math.sqrt(Math.pow(pos.x - node.x, 2) + Math.pow(pos.y - node.y, 2));
  187. difference = distanceToBorder - distanceToPoint;
  188. if (Math.abs(difference) < threshold) {
  189. break; // found
  190. }
  191. 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.
  192. if (direction > 0) {
  193. low = middle;
  194. }
  195. else {
  196. high = middle;
  197. }
  198. }
  199. else {
  200. if (direction > 0) {
  201. high = middle;
  202. }
  203. else {
  204. low = middle;
  205. }
  206. }
  207. iteration++;
  208. }
  209. pos.t = middle;
  210. return pos;
  211. }
  212. /**
  213. * Get the line width of the edge. Depends on width and whether one of the
  214. * connected nodes is selected.
  215. * @param {boolean} selected
  216. * @param {boolean} hover
  217. * @return {Number} width
  218. * @private
  219. */
  220. getLineWidth(selected, hover) {
  221. if (selected === true) {
  222. return Math.max(this.selectionWidth, 0.3 / this.body.view.scale);
  223. }
  224. else {
  225. if (hover === true) {
  226. return Math.max(this.hoverWidth, 0.3 / this.body.view.scale);
  227. }
  228. else {
  229. return Math.max(this.options.width, 0.3 / this.body.view.scale);
  230. }
  231. }
  232. }
  233. getColor(ctx, values, selected, hover) { // eslint-disable-line no-unused-vars
  234. if (values.inheritsColor !== false) {
  235. // when this is a loop edge, just use the 'from' method
  236. if ((values.inheritsColor === '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, values.opacity);
  243. toColor = util.overrideOpacity(this.to.options.color.border, values.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 (values.inheritsColor === "to") {
  257. return util.overrideOpacity(this.to.options.color.border, values.opacity);
  258. } else { // "from"
  259. return util.overrideOpacity(this.from.options.color.border, values.opacity);
  260. }
  261. } else {
  262. return util.overrideOpacity(values.color, values.opacity);
  263. }
  264. }
  265. /**
  266. * Draw a line from a node to itself, a circle
  267. *
  268. * @param {CanvasRenderingContext2D} ctx
  269. * @param {Array} values
  270. * @param {Number} x
  271. * @param {Number} y
  272. * @param {Number} radius
  273. * @private
  274. */
  275. _circle(ctx, values, x, y, radius) {
  276. // draw shadow if enabled
  277. this.enableShadow(ctx, values);
  278. // draw a circle
  279. ctx.beginPath();
  280. ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
  281. ctx.stroke();
  282. // disable shadows for other elements.
  283. this.disableShadow(ctx, values);
  284. }
  285. /**
  286. * Calculate the distance between a point (x3,y3) and a line segment from
  287. * (x1,y1) to (x2,y2).
  288. * x3,y3 is the point.
  289. * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment
  290. * @param {number} x1
  291. * @param {number} y1
  292. * @param {number} x2
  293. * @param {number} y2
  294. * @param {number} x3
  295. * @param {number} y3
  296. * @param {vis.Node} via
  297. * @param {Array} values
  298. * @returns {number}
  299. * @private
  300. */
  301. getDistanceToEdge(x1, y1, x2, y2, x3, y3, via, values) { // eslint-disable-line no-unused-vars
  302. let returnValue = 0;
  303. if (this.from != this.to) {
  304. returnValue = this._getDistanceToEdge(x1, y1, x2, y2, x3, y3, via)
  305. }
  306. else {
  307. let [x,y,radius] = this._getCircleData(undefined);
  308. let dx = x - x3;
  309. let dy = y - y3;
  310. returnValue = Math.abs(Math.sqrt(dx * dx + dy * dy) - radius);
  311. }
  312. if (this.labelModule.size.left < x3 &&
  313. this.labelModule.size.left + this.labelModule.size.width > x3 &&
  314. this.labelModule.size.top < y3 &&
  315. this.labelModule.size.top + this.labelModule.size.height > y3) {
  316. return 0;
  317. }
  318. else {
  319. return returnValue;
  320. }
  321. }
  322. _getDistanceToLine(x1, y1, x2, y2, x3, y3) {
  323. let px = x2 - x1;
  324. let py = y2 - y1;
  325. let something = px * px + py * py;
  326. let u = ((x3 - x1) * px + (y3 - y1) * py) / something;
  327. if (u > 1) {
  328. u = 1;
  329. }
  330. else if (u < 0) {
  331. u = 0;
  332. }
  333. let x = x1 + u * px;
  334. let y = y1 + u * py;
  335. let dx = x - x3;
  336. let dy = y - y3;
  337. //# Note: If the actual distance does not matter,
  338. //# if you only want to compare what this function
  339. //# returns to other results of this function, you
  340. //# can just return the squared distance instead
  341. //# (i.e. remove the sqrt) to gain a little performance
  342. return Math.sqrt(dx * dx + dy * dy);
  343. }
  344. /**
  345. * @param {CanvasRenderingContext2D} ctx
  346. * @param {string} position
  347. * @param {vis.Node} viaNode
  348. * @param {boolean} selected
  349. * @param {boolean} hover
  350. * @param {Array} values
  351. * @returns {{point: *, core: {x: number, y: number}, angle: *, length: number, type: *}}
  352. */
  353. getArrowData(ctx, position, viaNode, selected, hover, values) {
  354. // set lets
  355. let angle;
  356. let arrowPoint;
  357. let node1;
  358. let node2;
  359. let guideOffset;
  360. let scaleFactor;
  361. let type;
  362. let lineWidth = values.width;
  363. if (position === 'from') {
  364. node1 = this.from;
  365. node2 = this.to;
  366. guideOffset = 0.1;
  367. scaleFactor = values.fromArrowScale;
  368. type = values.fromArrowType;
  369. }
  370. else if (position === 'to') {
  371. node1 = this.to;
  372. node2 = this.from;
  373. guideOffset = -0.1;
  374. scaleFactor = values.toArrowScale;
  375. type = values.toArrowType;
  376. }
  377. else {
  378. node1 = this.to;
  379. node2 = this.from;
  380. scaleFactor = values.middleArrowScale;
  381. type = values.middleArrowType;
  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. } else {
  392. angle = Math.atan2((node1.y - node2.y), (node1.x - node2.x));
  393. arrowPoint = this.findBorderPosition(node1, ctx);
  394. }
  395. } else {
  396. angle = Math.atan2((node1.y - node2.y), (node1.x - node2.x));
  397. arrowPoint = this.getPoint(0.5, viaNode); // this is 0.6 to account for the size of the arrow.
  398. }
  399. } else {
  400. // draw circle
  401. let [x,y,radius] = this._getCircleData(ctx);
  402. if (position === 'from') {
  403. arrowPoint = this.findBorderPosition(this.from, ctx, { x, y, low: 0.25, high: 0.6, direction: -1 });
  404. angle = arrowPoint.t * -2 * Math.PI + 1.5 * Math.PI + 0.1 * Math.PI;
  405. } else if (position === 'to') {
  406. arrowPoint = this.findBorderPosition(this.from, ctx, { x, y, low: 0.6, high: 1.0, direction: 1 });
  407. angle = arrowPoint.t * -2 * Math.PI + 1.5 * Math.PI - 1.1 * Math.PI;
  408. } else {
  409. arrowPoint = 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. }
  413. let length = 15 * scaleFactor + 3 * lineWidth; // 3* lineWidth is the width of the edge.
  414. var xi = arrowPoint.x - length * 0.9 * Math.cos(angle);
  415. var yi = arrowPoint.y - length * 0.9 * Math.sin(angle);
  416. let arrowCore = { x: xi, y: yi };
  417. return { point: arrowPoint, core: arrowCore, angle: angle, length: length, type: type };
  418. }
  419. /**
  420. *
  421. * @param {CanvasRenderingContext2D} ctx
  422. * @param {Array<Object>} values
  423. * @param {boolean} selected
  424. * @param {boolean} hover
  425. * @param {Object} arrowData
  426. */
  427. drawArrowHead(ctx, values, selected, hover, arrowData) {
  428. // set style
  429. ctx.strokeStyle = this.getColor(ctx, values, selected, hover);
  430. ctx.fillStyle = ctx.strokeStyle;
  431. ctx.lineWidth = values.width;
  432. if (arrowData.type && arrowData.type.toLowerCase() === 'circle') {
  433. // draw circle at the end of the line
  434. ctx.circleEndpoint(arrowData.point.x, arrowData.point.y, arrowData.angle, arrowData.length);
  435. } else {
  436. // draw arrow at the end of the line
  437. ctx.arrowEndpoint(arrowData.point.x, arrowData.point.y, arrowData.angle, arrowData.length);
  438. }
  439. // draw shadow if enabled
  440. this.enableShadow(ctx, values);
  441. ctx.fill();
  442. // disable shadows for other elements.
  443. this.disableShadow(ctx, values);
  444. }
  445. enableShadow(ctx, values) {
  446. if (values.shadow === true) {
  447. ctx.shadowColor = values.shadowColor;
  448. ctx.shadowBlur = values.shadowSize;
  449. ctx.shadowOffsetX = values.shadowX;
  450. ctx.shadowOffsetY = values.shadowY;
  451. }
  452. }
  453. disableShadow(ctx, values) {
  454. if (values.shadow === true) {
  455. ctx.shadowColor = 'rgba(0,0,0,0)';
  456. ctx.shadowBlur = 0;
  457. ctx.shadowOffsetX = 0;
  458. ctx.shadowOffsetY = 0;
  459. }
  460. }
  461. }
  462. export default EdgeBase;