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.

604 lines
20 KiB

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