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.

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