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.

1468 lines
42 KiB

10 years ago
  1. var util = require('../../../../util');
  2. import Label from '../unified/label.js'
  3. /**
  4. * @class Edge
  5. *
  6. * A edge connects two nodes
  7. * @param {Object} properties Object with options. Must contain
  8. * At least options from and to.
  9. * Available options: from (number),
  10. * to (number), label (string, color (string),
  11. * width (number), style (string),
  12. * length (number), title (string)
  13. * @param {Network} network A Network object, used to find and edge to
  14. * nodes.
  15. * @param {Object} constants An object with default values for
  16. * example for the color
  17. */
  18. class Edge {
  19. constructor(options, body, globalOptions) {
  20. if (body === undefined) {
  21. throw "No body provided";
  22. }
  23. this.options = util.bridgeObject(globalOptions);
  24. this.body = body;
  25. // initialize variables
  26. this.id = undefined;
  27. this.fromId = undefined;
  28. this.toId = undefined;
  29. this.title = undefined;
  30. this.widthSelected = this.options.width * this.options.widthSelectionMultiplier;
  31. this.value = undefined;
  32. this.selected = false;
  33. this.hover = false;
  34. this.labelDirty = true;
  35. this.colorDirty = true;
  36. this.from = undefined; // a node
  37. this.to = undefined; // a node
  38. this.via = undefined; // a temp node
  39. this.fromBackup = undefined; // used to clean up after reconnect (used for manipulation)
  40. this.toBackup = undefined; // used to clean up after reconnect (used for manipulation)
  41. // we use this to be able to reconnect the edge to a cluster if its node is put into a cluster
  42. // by storing the original information we can revert to the original connection when the cluser is opened.
  43. this.fromArray = [];
  44. this.toArray = [];
  45. this.connected = false;
  46. this.labelModule = new Label(this.body, this.options);
  47. this.setOptions(options, true);
  48. this.controlNodesEnabled = false;
  49. this.controlNodes = {from: undefined, to: undefined, positions: {}};
  50. this.connectedNode = undefined;
  51. }
  52. /**
  53. * Set or overwrite options for the edge
  54. * @param {Object} options an object with options
  55. * @param doNotEmit
  56. */
  57. setOptions(options, doNotEmit = false) {
  58. if (!options) {
  59. return;
  60. }
  61. this.colorDirty = true;
  62. var fields = [
  63. 'font',
  64. 'hidden',
  65. 'hoverWidth',
  66. 'label',
  67. 'length',
  68. 'line',
  69. 'opacity',
  70. 'physics',
  71. 'scaling',
  72. 'selfReferenceSize',
  73. 'value',
  74. 'width',
  75. 'widthMin',
  76. 'widthMax',
  77. 'widthSelectionMultiplier'
  78. ];
  79. util.selectiveDeepExtend(fields, this.options, options);
  80. util.mergeOptions(this.options, options, 'smooth');
  81. util.mergeOptions(this.options, options, 'dashes');
  82. if (options.arrows !== undefined) {
  83. util.mergeOptions(this.options.arrows, options.arrows, 'to');
  84. util.mergeOptions(this.options.arrows, options.arrows, 'middle');
  85. util.mergeOptions(this.options.arrows, options.arrows, 'from');
  86. }
  87. if (options.id !== undefined) {this.id = options.id;}
  88. if (options.from !== undefined) {this.fromId = options.from;}
  89. if (options.to !== undefined) {this.toId = options.to;}
  90. if (options.title !== undefined) {this.title = options.title;}
  91. if (options.value !== undefined) {this.value = options.value;}
  92. if (options.color !== undefined) {
  93. if (util.isString(options.color)) {
  94. this.options.color.color = options.color;
  95. this.options.color.highlight = options.color;
  96. }
  97. else {
  98. if (options.color.color !== undefined) {
  99. this.options.color.color = options.color.color;
  100. }
  101. if (options.color.highlight !== undefined) {
  102. this.options.color.highlight = options.color.highlight;
  103. }
  104. if (options.color.hover !== undefined) {
  105. this.options.color.hover = options.color.hover;
  106. }
  107. }
  108. // inherit colors
  109. if (options.color.inherit === undefined) {
  110. this.options.color.inherit.enabled = false;
  111. }
  112. else {
  113. util.mergeOptions(this.options.color, options.color, 'inherit');
  114. }
  115. }
  116. // A node is connected when it has a from and to node that both exist in the network.body.nodes.
  117. this.connect();
  118. this.widthSelected = this.options.width * this.options.widthSelectionMultiplier;
  119. this.setupSmoothEdges(doNotEmit);
  120. this.labelModule.setOptions(this.options);
  121. }
  122. /**
  123. * Bezier curves require an anchor point to calculate the smooth flow. These points are nodes. These nodes are invisible but
  124. * are used for the force calculation.
  125. *
  126. * @private
  127. */
  128. setupSmoothEdges(doNotEmit = false) {
  129. var changedData = false;
  130. if (this.options.smooth.enabled == true && this.options.smooth.dynamic == true) {
  131. if (this.via === undefined) {
  132. changedData = true;
  133. var nodeId = "edgeId:" + this.id;
  134. var node = this.body.functions.createNode({
  135. id: nodeId,
  136. mass: 1,
  137. shape: 'circle',
  138. image: "",
  139. physics:true,
  140. hidden:true
  141. });
  142. this.body.nodes[nodeId] = node;
  143. this.via = node;
  144. this.via.parentEdgeId = this.id;
  145. this.positionBezierNode();
  146. }
  147. }
  148. else {
  149. if (this.via !== undefined) {
  150. delete this.body.nodes[this.via.id];
  151. this.via = undefined;
  152. changedData = true;
  153. }
  154. }
  155. // node has been added or deleted
  156. if (changedData === true && doNotEmit === false) {
  157. this.body.emitter.emit("_dataChanged");
  158. }
  159. }
  160. /**
  161. * Connect an edge to its nodes
  162. */
  163. connect() {
  164. this.disconnect();
  165. this.from = this.body.nodes[this.fromId] || undefined;
  166. this.to = this.body.nodes[this.toId] || undefined;
  167. this.connected = (this.from !== undefined && this.to !== undefined);
  168. if (this.connected === true) {
  169. this.from.attachEdge(this);
  170. this.to.attachEdge(this);
  171. }
  172. else {
  173. if (this.from) {
  174. this.from.detachEdge(this);
  175. }
  176. if (this.to) {
  177. this.to.detachEdge(this);
  178. }
  179. }
  180. }
  181. /**
  182. * Disconnect an edge from its nodes
  183. */
  184. disconnect() {
  185. if (this.from) {
  186. this.from.detachEdge(this);
  187. this.from = undefined;
  188. }
  189. if (this.to) {
  190. this.to.detachEdge(this);
  191. this.to = undefined;
  192. }
  193. this.connected = false;
  194. }
  195. /**
  196. * get the title of this edge.
  197. * @return {string} title The title of the edge, or undefined when no title
  198. * has been set.
  199. */
  200. getTitle() {
  201. return typeof this.title === "function" ? this.title() : this.title;
  202. }
  203. /**
  204. * check if this node is selecte
  205. * @return {boolean} selected True if node is selected, else false
  206. */
  207. isSelected() {
  208. return this.selected;
  209. }
  210. /**
  211. * Retrieve the value of the edge. Can be undefined
  212. * @return {Number} value
  213. */
  214. getValue() {
  215. return this.value;
  216. }
  217. /**
  218. * Adjust the value range of the edge. The edge will adjust it's width
  219. * based on its value.
  220. * @param {Number} min
  221. * @param {Number} max
  222. * @param total
  223. */
  224. setValueRange(min, max, total) {
  225. if (this.value !== undefined) {
  226. var scale = this.options.scaling.customScalingFunction(min, max, total, this.value);
  227. var widthDiff = this.options.scaling.max - this.options.scaling.min;
  228. if (this.options.scaling.label.enabled == true) {
  229. var fontDiff = this.options.scaling.label.max - this.options.scaling.label.min;
  230. this.options.font.size = this.options.scaling.label.min + scale * fontDiff;
  231. }
  232. this.options.width = this.options.scaling.min + scale * widthDiff;
  233. }
  234. }
  235. /**
  236. * Redraw a edge
  237. * Draw this edge in the given canvas
  238. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  239. * @param {CanvasRenderingContext2D} ctx
  240. */
  241. draw(ctx) {
  242. let via = this.drawLine(ctx);
  243. this.drawArrows(ctx, via);
  244. this.drawLabel (ctx, via);
  245. }
  246. drawLine(ctx) {
  247. if (this.options.dashes.enabled === false) {
  248. return this._drawLine(ctx);
  249. }
  250. else {
  251. return this._drawDashLine(ctx);
  252. }
  253. }
  254. drawArrows(ctx, viaNode) {
  255. if (this.options.arrows.from.enabled === true) {this._drawArrowHead(ctx,'from', viaNode);}
  256. if (this.options.arrows.middle.enabled === true) {this._drawArrowHead(ctx,'middle', viaNode);}
  257. if (this.options.arrows.to.enabled === true) {this._drawArrowHead(ctx,'to', viaNode);}
  258. }
  259. drawLabel(ctx, viaNode) {
  260. if (this.options.label !== undefined) {
  261. // set style
  262. var node1 = this.from;
  263. var node2 = this.to;
  264. var selected = (this.from.selected || this.to.selected || this.selected);
  265. if (node1.id != node2.id) {
  266. var point = this._pointOnEdge(0.5, viaNode);
  267. ctx.save();
  268. // if the label has to be rotated:
  269. if (this.options.font.align != "horizontal") {
  270. this.labelModule.calculateLabelSize(ctx,selected,point.x,point.y);
  271. ctx.translate(point.x, this.labelModule.size.yLine);
  272. this._rotateForLabelAlignment(ctx);
  273. }
  274. // draw the label
  275. this.labelModule.draw(ctx, point.x, point.y, selected);
  276. ctx.restore();
  277. }
  278. else {
  279. var x, y;
  280. var radius = this.options.selfReferenceSize;
  281. if (node1.width > node1.height) {
  282. x = node1.x + node1.width * 0.5;
  283. y = node1.y - radius;
  284. }
  285. else {
  286. x = node1.x + radius;
  287. y = node1.y - node1.height * 0.5;
  288. }
  289. point = this._pointOnCircle(x, y, radius, 0.125);
  290. this.labelModule.draw(ctx, point.x, point.y, selected);
  291. }
  292. }
  293. }
  294. /**
  295. * Check if this object is overlapping with the provided object
  296. * @param {Object} obj an object with parameters left, top
  297. * @return {boolean} True if location is located on the edge
  298. */
  299. isOverlappingWith(obj) {
  300. if (this.connected) {
  301. var distMax = 10;
  302. var xFrom = this.from.x;
  303. var yFrom = this.from.y;
  304. var xTo = this.to.x;
  305. var yTo = this.to.y;
  306. var xObj = obj.left;
  307. var yObj = obj.top;
  308. var dist = this._getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj);
  309. return (dist < distMax);
  310. }
  311. else {
  312. return false
  313. }
  314. }
  315. _getColor(ctx) {
  316. var colorObj = this.options.color;
  317. if (colorObj.inherit.enabled === true) {
  318. if (colorObj.inherit.useGradients == true) {
  319. var grd = ctx.createLinearGradient(this.from.x, this.from.y, this.to.x, this.to.y);
  320. var fromColor, toColor;
  321. fromColor = this.from.options.color.highlight.border;
  322. toColor = this.to.options.color.highlight.border;
  323. if (this.from.selected == false && this.to.selected == false) {
  324. fromColor = util.overrideOpacity(this.from.options.color.border, this.options.color.opacity);
  325. toColor = util.overrideOpacity(this.to.options.color.border, this.options.color.opacity);
  326. }
  327. else if (this.from.selected == true && this.to.selected == false) {
  328. toColor = this.to.options.color.border;
  329. }
  330. else if (this.from.selected == false && this.to.selected == true) {
  331. fromColor = this.from.options.color.border;
  332. }
  333. grd.addColorStop(0, fromColor);
  334. grd.addColorStop(1, toColor);
  335. // -------------------- this returns -------------------- //
  336. return grd;
  337. }
  338. if (this.colorDirty === true) {
  339. if (colorObj.inherit.source == "to") {
  340. colorObj.highlight = this.to.options.color.highlight.border;
  341. colorObj.hover = this.to.options.color.hover.border;
  342. colorObj.color = util.overrideOpacity(this.to.options.color.border, this.options.color.opacity);
  343. }
  344. else { // (this.options.color.inherit.source == "from") {
  345. colorObj.highlight = this.from.options.color.highlight.border;
  346. colorObj.hover = this.from.options.color.hover.border;
  347. colorObj.color = util.overrideOpacity(this.from.options.color.border, this.options.color.opacity);
  348. }
  349. }
  350. }
  351. // if color inherit is on and gradients are used, the function has already returned by now.
  352. this.colorDirty = false;
  353. if (this.selected == true) {
  354. return colorObj.highlight;
  355. }
  356. else if (this.hover == true) {
  357. return colorObj.hover;
  358. }
  359. else {
  360. return colorObj.color;
  361. }
  362. }
  363. /**
  364. * Redraw a edge as a line
  365. * Draw this edge in the given canvas
  366. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  367. * @param {CanvasRenderingContext2D} ctx
  368. * @private
  369. */
  370. _drawLine(ctx) {
  371. // set style
  372. ctx.strokeStyle = this._getColor(ctx);
  373. ctx.lineWidth = this._getLineWidth();
  374. var via;
  375. if (this.from != this.to) {
  376. // draw line
  377. via = this._line(ctx);
  378. }
  379. else {
  380. var x, y;
  381. var radius = this.options.selfReferenceSize;
  382. var node = this.from;
  383. if (!node.width) {
  384. node.resize(ctx);
  385. }
  386. if (node.width > node.height) {
  387. x = node.x + node.width * 0.5;
  388. y = node.y - radius;
  389. }
  390. else {
  391. x = node.x + radius;
  392. y = node.y - node.height * 0.5;
  393. }
  394. this._circle(ctx, x, y, radius);
  395. }
  396. return via;
  397. }
  398. /**
  399. * Get the line width of the edge. Depends on width and whether one of the
  400. * connected nodes is selected.
  401. * @return {Number} width
  402. * @private
  403. */
  404. _getLineWidth() {
  405. if (this.selected == true) {
  406. return Math.max(Math.min(this.widthSelected, this.options.widthMax), 0.3 * this.networkScaleInv);
  407. }
  408. else {
  409. if (this.hover == true) {
  410. return Math.max(Math.min(this.options.hoverWidth, this.options.widthMax), 0.3 * this.networkScaleInv);
  411. }
  412. else {
  413. return Math.max(this.options.width, 0.3 * this.networkScaleInv);
  414. }
  415. }
  416. }
  417. _getViaCoordinates() {
  418. if (this.options.smooth.dynamic == true && this.options.smooth.enabled == true) {
  419. return this.via;
  420. }
  421. else if (this.options.smooth.enabled == false) {
  422. return {x: 0, y: 0};
  423. }
  424. else {
  425. let xVia = undefined;
  426. let yVia = undefined;
  427. let factor = this.options.smooth.roundness;
  428. let type = this.options.smooth.type;
  429. let dx = Math.abs(this.from.x - this.to.x);
  430. let dy = Math.abs(this.from.y - this.to.y);
  431. if (type == 'discrete' || type == 'diagonalCross') {
  432. if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) {
  433. if (this.from.y > this.to.y) {
  434. if (this.from.x < this.to.x) {
  435. xVia = this.from.x + factor * dy;
  436. yVia = this.from.y - factor * dy;
  437. }
  438. else if (this.from.x > this.to.x) {
  439. xVia = this.from.x - factor * dy;
  440. yVia = this.from.y - factor * dy;
  441. }
  442. }
  443. else if (this.from.y < this.to.y) {
  444. if (this.from.x < this.to.x) {
  445. xVia = this.from.x + factor * dy;
  446. yVia = this.from.y + factor * dy;
  447. }
  448. else if (this.from.x > this.to.x) {
  449. xVia = this.from.x - factor * dy;
  450. yVia = this.from.y + factor * dy;
  451. }
  452. }
  453. if (type == "discrete") {
  454. xVia = dx < factor * dy ? this.from.x : xVia;
  455. }
  456. }
  457. else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) {
  458. if (this.from.y > this.to.y) {
  459. if (this.from.x < this.to.x) {
  460. xVia = this.from.x + factor * dx;
  461. yVia = this.from.y - factor * dx;
  462. }
  463. else if (this.from.x > this.to.x) {
  464. xVia = this.from.x - factor * dx;
  465. yVia = this.from.y - factor * dx;
  466. }
  467. }
  468. else if (this.from.y < this.to.y) {
  469. if (this.from.x < this.to.x) {
  470. xVia = this.from.x + factor * dx;
  471. yVia = this.from.y + factor * dx;
  472. }
  473. else if (this.from.x > this.to.x) {
  474. xVia = this.from.x - factor * dx;
  475. yVia = this.from.y + factor * dx;
  476. }
  477. }
  478. if (type == "discrete") {
  479. yVia = dy < factor * dx ? this.from.y : yVia;
  480. }
  481. }
  482. }
  483. else if (type == "straightCross") {
  484. if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) { // up - down
  485. xVia = this.from.x;
  486. if (this.from.y < this.to.y) {
  487. yVia = this.to.y - (1 - factor) * dy;
  488. }
  489. else {
  490. yVia = this.to.y + (1 - factor) * dy;
  491. }
  492. }
  493. else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) { // left - right
  494. if (this.from.x < this.to.x) {
  495. xVia = this.to.x - (1 - factor) * dx;
  496. }
  497. else {
  498. xVia = this.to.x + (1 - factor) * dx;
  499. }
  500. yVia = this.from.y;
  501. }
  502. }
  503. else if (type == 'horizontal') {
  504. if (this.from.x < this.to.x) {
  505. xVia = this.to.x - (1 - factor) * dx;
  506. }
  507. else {
  508. xVia = this.to.x + (1 - factor) * dx;
  509. }
  510. yVia = this.from.y;
  511. }
  512. else if (type == 'vertical') {
  513. xVia = this.from.x;
  514. if (this.from.y < this.to.y) {
  515. yVia = this.to.y - (1 - factor) * dy;
  516. }
  517. else {
  518. yVia = this.to.y + (1 - factor) * dy;
  519. }
  520. }
  521. else if (type == 'curvedCW') {
  522. dx = this.to.x - this.from.x;
  523. dy = this.from.y - this.to.y;
  524. let radius = Math.sqrt(dx * dx + dy * dy);
  525. let pi = Math.PI;
  526. let originalAngle = Math.atan2(dy, dx);
  527. let myAngle = (originalAngle + ((factor * 0.5) + 0.5) * pi) % (2 * pi);
  528. xVia = this.from.x + (factor * 0.5 + 0.5) * radius * Math.sin(myAngle);
  529. yVia = this.from.y + (factor * 0.5 + 0.5) * radius * Math.cos(myAngle);
  530. }
  531. else if (type == 'curvedCCW') {
  532. dx = this.to.x - this.from.x;
  533. dy = this.from.y - this.to.y;
  534. let radius = Math.sqrt(dx * dx + dy * dy);
  535. let pi = Math.PI;
  536. let originalAngle = Math.atan2(dy, dx);
  537. let myAngle = (originalAngle + ((-factor * 0.5) + 0.5) * pi) % (2 * pi);
  538. xVia = this.from.x + (factor * 0.5 + 0.5) * radius * Math.sin(myAngle);
  539. yVia = this.from.y + (factor * 0.5 + 0.5) * radius * Math.cos(myAngle);
  540. }
  541. else { // continuous
  542. if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) {
  543. if (this.from.y > this.to.y) {
  544. if (this.from.x < this.to.x) {
  545. xVia = this.from.x + factor * dy;
  546. yVia = this.from.y - factor * dy;
  547. xVia = this.to.x < xVia ? this.to.x : xVia;
  548. }
  549. else if (this.from.x > this.to.x) {
  550. xVia = this.from.x - factor * dy;
  551. yVia = this.from.y - factor * dy;
  552. xVia = this.to.x > xVia ? this.to.x : xVia;
  553. }
  554. }
  555. else if (this.from.y < this.to.y) {
  556. if (this.from.x < this.to.x) {
  557. xVia = this.from.x + factor * dy;
  558. yVia = this.from.y + factor * dy;
  559. xVia = this.to.x < xVia ? this.to.x : xVia;
  560. }
  561. else if (this.from.x > this.to.x) {
  562. xVia = this.from.x - factor * dy;
  563. yVia = this.from.y + factor * dy;
  564. xVia = this.to.x > xVia ? this.to.x : xVia;
  565. }
  566. }
  567. }
  568. else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) {
  569. if (this.from.y > this.to.y) {
  570. if (this.from.x < this.to.x) {
  571. xVia = this.from.x + factor * dx;
  572. yVia = this.from.y - factor * dx;
  573. yVia = this.to.y > yVia ? this.to.y : yVia;
  574. }
  575. else if (this.from.x > this.to.x) {
  576. xVia = this.from.x - factor * dx;
  577. yVia = this.from.y - factor * dx;
  578. yVia = this.to.y > yVia ? this.to.y : yVia;
  579. }
  580. }
  581. else if (this.from.y < this.to.y) {
  582. if (this.from.x < this.to.x) {
  583. xVia = this.from.x + factor * dx;
  584. yVia = this.from.y + factor * dx;
  585. yVia = this.to.y < yVia ? this.to.y : yVia;
  586. }
  587. else if (this.from.x > this.to.x) {
  588. xVia = this.from.x - factor * dx;
  589. yVia = this.from.y + factor * dx;
  590. yVia = this.to.y < yVia ? this.to.y : yVia;
  591. }
  592. }
  593. }
  594. }
  595. return {x: xVia, y: yVia};
  596. }
  597. }
  598. /**
  599. * Draw a line between two nodes
  600. * @param {CanvasRenderingContext2D} ctx
  601. * @private
  602. */
  603. _line(ctx) {
  604. // draw a straight line
  605. ctx.beginPath();
  606. ctx.moveTo(this.from.x, this.from.y);
  607. if (this.options.smooth.enabled == true) {
  608. if (this.options.smooth.dynamic == false) {
  609. var via = this._getViaCoordinates();
  610. if (via.x === undefined) {
  611. ctx.lineTo(this.to.x, this.to.y);
  612. ctx.stroke();
  613. return undefined;
  614. }
  615. else {
  616. // this.via.x = via.x;
  617. // this.via.y = via.y;
  618. ctx.quadraticCurveTo(via.x, via.y, this.to.x, this.to.y);
  619. ctx.stroke();
  620. //ctx.circle(via.x,via.y,2)
  621. //ctx.stroke();
  622. return via;
  623. }
  624. }
  625. else {
  626. ctx.quadraticCurveTo(this.via.x, this.via.y, this.to.x, this.to.y);
  627. ctx.stroke();
  628. return this.via;
  629. }
  630. }
  631. else {
  632. ctx.lineTo(this.to.x, this.to.y);
  633. ctx.stroke();
  634. return undefined;
  635. }
  636. }
  637. /**
  638. * Draw a line from a node to itself, a circle
  639. * @param {CanvasRenderingContext2D} ctx
  640. * @param {Number} x
  641. * @param {Number} y
  642. * @param {Number} radius
  643. * @private
  644. */
  645. _circle(ctx, x, y, radius) {
  646. // draw a circle
  647. ctx.beginPath();
  648. ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
  649. ctx.stroke();
  650. }
  651. /**
  652. * Draw label with white background and with the middle at (x, y)
  653. * @param {CanvasRenderingContext2D} ctx
  654. * @param {String} text
  655. * @param {Number} x
  656. * @param {Number} y
  657. * @private
  658. */
  659. /**
  660. * Rotates the canvas so the text is most readable
  661. * @param {CanvasRenderingContext2D} ctx
  662. * @private
  663. */
  664. _rotateForLabelAlignment(ctx) {
  665. var dy = this.from.y - this.to.y;
  666. var dx = this.from.x - this.to.x;
  667. var angleInDegrees = Math.atan2(dy, dx);
  668. // rotate so label it is readable
  669. if ((angleInDegrees < -1 && dx < 0) || (angleInDegrees > 0 && dx < 0)) {
  670. angleInDegrees = angleInDegrees + Math.PI;
  671. }
  672. ctx.rotate(angleInDegrees);
  673. }
  674. /**
  675. * Redraw a edge as a dashes line
  676. * Draw this edge in the given canvas
  677. * @author David Jordan
  678. * @date 2012-08-08
  679. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  680. * @param {CanvasRenderingContext2D} ctx
  681. * @private
  682. */
  683. _drawDashLine(ctx) {
  684. // set style
  685. ctx.strokeStyle = this._getColor(ctx);
  686. ctx.lineWidth = this._getLineWidth();
  687. var via = undefined;
  688. // only firefox and chrome support this method, else we use the legacy one.
  689. if (ctx.setLineDash !== undefined) {
  690. ctx.save();
  691. // configure the dash pattern
  692. var pattern = [0];
  693. if (this.options.dashes.length !== undefined && this.options.dashes.gap !== undefined) {
  694. pattern = [this.options.dashes.length, this.options.dashes.gap];
  695. }
  696. else {
  697. pattern = [5, 5];
  698. }
  699. // set dash settings for chrome or firefox
  700. ctx.setLineDash(pattern);
  701. ctx.lineDashOffset = 0;
  702. // draw the line
  703. via = this._line(ctx);
  704. // restore the dash settings.
  705. ctx.setLineDash([0]);
  706. ctx.lineDashOffset = 0;
  707. ctx.restore();
  708. }
  709. else { // unsupporting smooth lines
  710. // draw dashes line
  711. ctx.beginPath();
  712. ctx.lineCap = 'round';
  713. if (this.options.dashes.altLength !== undefined) //If an alt dash value has been set add to the array this value
  714. {
  715. ctx.dashesLine(this.from.x, this.from.y, this.to.x, this.to.y,
  716. [this.options.dashes.length, this.options.dashes.gap, this.options.dashes.altLength, this.options.dashes.gap]);
  717. }
  718. else if (this.options.dashes.length !== undefined && this.options.dashes.gap !== undefined) //If a dash and gap value has been set add to the array this value
  719. {
  720. ctx.dashesLine(this.from.x, this.from.y, this.to.x, this.to.y,
  721. [this.options.dashes.length, this.options.dashes.gap]);
  722. }
  723. else //If all else fails draw a line
  724. {
  725. ctx.moveTo(this.from.x, this.from.y);
  726. ctx.lineTo(this.to.x, this.to.y);
  727. }
  728. ctx.stroke();
  729. }
  730. return via;
  731. }
  732. /**
  733. * Combined function of pointOnLine and pointOnBezier. This gives the coordinates of a point on the line at a certain percentage of the way
  734. * @param percentage
  735. * @param via
  736. * @returns {{x: number, y: number}}
  737. * @private
  738. */
  739. _pointOnEdge(percentage, via = this._getViaCoordinates()) {
  740. if (this.options.smooth.enabled == false) {
  741. return {
  742. x: (1 - percentage) * this.from.x + percentage * this.to.x,
  743. y: (1 - percentage) * this.from.y + percentage * this.to.y
  744. }
  745. }
  746. else {
  747. var t = percentage;
  748. var x = Math.pow(1 - t, 2) * this.from.x + (2 * t * (1 - t)) * via.x + Math.pow(t, 2) * this.to.x;
  749. var y = Math.pow(1 - t, 2) * this.from.y + (2 * t * (1 - t)) * via.y + Math.pow(t, 2) * this.to.y;
  750. return {x: x, y: y};
  751. }
  752. }
  753. /**
  754. * Get a point on a circle
  755. * @param {Number} x
  756. * @param {Number} y
  757. * @param {Number} radius
  758. * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
  759. * @return {Object} point
  760. * @private
  761. */
  762. _pointOnCircle(x, y, radius, percentage) {
  763. var angle = percentage * 2 * Math.PI;
  764. return {
  765. x: x + radius * Math.cos(angle),
  766. y: y - radius * Math.sin(angle)
  767. }
  768. }
  769. /**
  770. * This function uses binary search to look for the point where the bezier curve crosses the border of the node.
  771. *
  772. * @param nearNode
  773. * @param ctx
  774. * @param viaNode
  775. * @param nearNode
  776. * @param ctx
  777. * @param viaNode
  778. * @param nearNode
  779. * @param ctx
  780. * @param viaNode
  781. */
  782. _findBorderPositionBezier(nearNode, ctx, viaNode = this._getViaCoordinates()) {
  783. var maxIterations = 10;
  784. var iteration = 0;
  785. var low = 0;
  786. var high = 1;
  787. var pos, angle, distanceToBorder, distanceToPoint, difference;
  788. var threshold = 0.2;
  789. var node = this.to;
  790. var from = false;
  791. if (nearNode.id === this.from.id) {
  792. node = this.from;
  793. from = true;
  794. }
  795. while (low <= high && iteration < maxIterations) {
  796. var middle = (low + high) * 0.5;
  797. pos = this._pointOnEdge(middle, viaNode);
  798. angle = Math.atan2((node.y - pos.y), (node.x - pos.x));
  799. distanceToBorder = node.distanceToBorder(ctx, angle);
  800. distanceToPoint = Math.sqrt(Math.pow(pos.x - node.x, 2) + Math.pow(pos.y - node.y, 2));
  801. difference = distanceToBorder - distanceToPoint;
  802. if (Math.abs(difference) < threshold) {
  803. break; // found
  804. }
  805. 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.
  806. if (from == false) {
  807. low = middle;
  808. }
  809. else {
  810. high = middle;
  811. }
  812. }
  813. else {
  814. if (from == false) {
  815. high = middle;
  816. }
  817. else {
  818. low = middle;
  819. }
  820. }
  821. iteration++;
  822. }
  823. pos.t = middle;
  824. return pos;
  825. }
  826. /**
  827. * This function uses binary search to look for the point where the circle crosses the border of the node.
  828. * @param x
  829. * @param y
  830. * @param radius
  831. * @param node
  832. * @param low
  833. * @param high
  834. * @param direction
  835. * @param ctx
  836. * @returns {*}
  837. * @private
  838. */
  839. _findBorderPositionCircle(x,y,radius,node,low,high,direction,ctx) {
  840. var maxIterations = 10;
  841. var iteration = 0;
  842. var pos, angle, distanceToBorder, distanceToPoint, difference;
  843. var threshold = 0.05;
  844. while (low <= high && iteration < maxIterations) {
  845. var middle = (low + high) * 0.5;
  846. pos = this._pointOnCircle(x,y,radius,middle);
  847. angle = Math.atan2((node.y - pos.y), (node.x - pos.x));
  848. distanceToBorder = node.distanceToBorder(ctx, angle);
  849. distanceToPoint = Math.sqrt(Math.pow(pos.x - node.x, 2) + Math.pow(pos.y - node.y, 2));
  850. difference = distanceToBorder - distanceToPoint;
  851. if (Math.abs(difference) < threshold) {
  852. break; // found
  853. }
  854. 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.
  855. if (direction > 0) {
  856. low = middle;
  857. }
  858. else {
  859. high = middle;
  860. }
  861. }
  862. else {
  863. if (direction > 0) {
  864. high = middle;
  865. }
  866. else {
  867. low = middle;
  868. }
  869. }
  870. iteration++;
  871. }
  872. pos.t = middle;
  873. return pos;
  874. }
  875. /**
  876. *
  877. * @param ctx
  878. * @param position
  879. * @param viaNode
  880. */
  881. _drawArrowHead(ctx,position,viaNode) {
  882. // set style
  883. ctx.strokeStyle = this._getColor(ctx);
  884. ctx.fillStyle = ctx.strokeStyle;
  885. ctx.lineWidth = this._getLineWidth();
  886. // set vars
  887. var angle;
  888. var length;
  889. var arrowPos;
  890. var node1;
  891. var node2;
  892. var guideOffset;
  893. var scaleFactor;
  894. if (position == 'from') {
  895. node1 = this.from;
  896. node2 = this.to;
  897. guideOffset = 0.1;
  898. scaleFactor = this.options.arrows.from.scaleFactor;
  899. }
  900. else if (position == 'to') {
  901. node1 = this.to;
  902. node2 = this.from;
  903. guideOffset = -0.1;
  904. scaleFactor = this.options.arrows.to.scaleFactor;
  905. }
  906. else {
  907. node1 = this.to;
  908. node2 = this.from;
  909. scaleFactor = this.options.arrows.middle.scaleFactor;
  910. }
  911. // if not connected to itself
  912. if (node1 != node2) {
  913. if (position !== 'middle') {
  914. // draw arrow head
  915. if (this.options.smooth.enabled == true) {
  916. arrowPos = this._findBorderPositionBezier(node1, ctx, viaNode);
  917. var guidePos = this._pointOnEdge(Math.max(0.0,Math.min(1.0,arrowPos.t + guideOffset)), viaNode);
  918. angle = Math.atan2((arrowPos.y - guidePos.y), (arrowPos.x - guidePos.x));
  919. }
  920. else {
  921. angle = Math.atan2((node1.y - node2.y), (node1.x - node2.x));
  922. var dx = (node1.x - node2.x);
  923. var dy = (node1.y - node2.y);
  924. var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
  925. var toBorderDist = this.to.distanceToBorder(ctx, angle);
  926. var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength;
  927. arrowPos = {};
  928. arrowPos.x = (1 - toBorderPoint) * node2.x + toBorderPoint * node1.x;
  929. arrowPos.y = (1 - toBorderPoint) * node2.y + toBorderPoint * node1.y;
  930. }
  931. }
  932. else {
  933. angle = Math.atan2((node1.y - node2.y), (node1.x - node2.x));
  934. arrowPos = this._pointOnEdge(0.6, viaNode); // this is 0.6 to account for the size of the arrow.
  935. }
  936. // draw arrow at the end of the line
  937. length = (10 + 5 * this.options.width) * scaleFactor;
  938. ctx.arrow(arrowPos.x, arrowPos.y, angle, length);
  939. ctx.fill();
  940. ctx.stroke();
  941. }
  942. else {
  943. // draw circle
  944. var angle, point;
  945. var x, y;
  946. var radius = this.options.selfReferenceSize;
  947. if (!node1.width) {
  948. node1.resize(ctx);
  949. }
  950. // get circle coordinates
  951. if (node1.width > node1.height) {
  952. x = node1.x + node1.width * 0.5;
  953. y = node1.y - radius;
  954. }
  955. else {
  956. x = node1.x + radius;
  957. y = node1.y - node1.height * 0.5;
  958. }
  959. if (position == 'from') {
  960. point = this._findBorderPositionCircle(x, y, radius, node1, 0.25, 0.6, -1, ctx);
  961. angle = point.t * -2 * Math.PI + 1.5 * Math.PI + 0.1 * Math.PI;
  962. }
  963. else if (position == 'to') {
  964. point = this._findBorderPositionCircle(x, y, radius, node1, 0.6, 0.8, 1, ctx);
  965. angle = point.t * -2 * Math.PI + 1.5 * Math.PI - 1.1 * Math.PI;
  966. }
  967. else {
  968. point = this._pointOnCircle(x,y,radius,0.175);
  969. angle = 3.9269908169872414; // == 0.175 * -2 * Math.PI + 1.5 * Math.PI + 0.1 * Math.PI;
  970. }
  971. // draw the arrowhead
  972. var length = (10 + 5 * this.options.width) * scaleFactor;
  973. ctx.arrow(point.x, point.y, angle, length);
  974. ctx.fill();
  975. ctx.stroke();
  976. }
  977. }
  978. /**
  979. * Calculate the distance between a point (x3,y3) and a line segment from
  980. * (x1,y1) to (x2,y2).
  981. * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment
  982. * @param {number} x1
  983. * @param {number} y1
  984. * @param {number} x2
  985. * @param {number} y2
  986. * @param {number} x3
  987. * @param {number} y3
  988. * @private
  989. */
  990. _getDistanceToEdge(x1, y1, x2, y2, x3, y3) { // x3,y3 is the point
  991. var returnValue = 0;
  992. if (this.from != this.to) {
  993. if (this.options.smooth.enabled == true) {
  994. var xVia, yVia;
  995. if (this.options.smooth.enabled == true && this.options.smooth.dynamic == true) {
  996. xVia = this.via.x;
  997. yVia = this.via.y;
  998. }
  999. else {
  1000. var via = this._getViaCoordinates();
  1001. xVia = via.x;
  1002. yVia = via.y;
  1003. }
  1004. var minDistance = 1e9;
  1005. var distance;
  1006. var i, t, x, y;
  1007. var lastX = x1;
  1008. var lastY = y1;
  1009. for (i = 1; i < 10; i++) {
  1010. t = 0.1 * i;
  1011. x = Math.pow(1 - t, 2) * x1 + (2 * t * (1 - t)) * xVia + Math.pow(t, 2) * x2;
  1012. y = Math.pow(1 - t, 2) * y1 + (2 * t * (1 - t)) * yVia + Math.pow(t, 2) * y2;
  1013. if (i > 0) {
  1014. distance = this._getDistanceToLine(lastX, lastY, x, y, x3, y3);
  1015. minDistance = distance < minDistance ? distance : minDistance;
  1016. }
  1017. lastX = x;
  1018. lastY = y;
  1019. }
  1020. returnValue = minDistance;
  1021. }
  1022. else {
  1023. returnValue = this._getDistanceToLine(x1, y1, x2, y2, x3, y3);
  1024. }
  1025. }
  1026. else {
  1027. var x, y, dx, dy;
  1028. var radius = this.options.selfReferenceSize;
  1029. var node = this.from;
  1030. if (node.width > node.height) {
  1031. x = node.x + 0.5 * node.width;
  1032. y = node.y - radius;
  1033. }
  1034. else {
  1035. x = node.x + radius;
  1036. y = node.y - 0.5 * node.height;
  1037. }
  1038. dx = x - x3;
  1039. dy = y - y3;
  1040. returnValue = Math.abs(Math.sqrt(dx * dx + dy * dy) - radius);
  1041. }
  1042. if (this.labelModule.size.left < x3 &&
  1043. this.labelModule.size.left + this.labelModule.size.width > x3 &&
  1044. this.labelModule.size.top < y3 &&
  1045. this.labelModule.size.top + this.labelModule.size.height > y3) {
  1046. return 0;
  1047. }
  1048. else {
  1049. return returnValue;
  1050. }
  1051. }
  1052. _getDistanceToLine(x1, y1, x2, y2, x3, y3) {
  1053. var px = x2 - x1;
  1054. var py = y2 - y1;
  1055. var something = px * px + py * py;
  1056. var u = ((x3 - x1) * px + (y3 - y1) * py) / something;
  1057. if (u > 1) {
  1058. u = 1;
  1059. }
  1060. else if (u < 0) {
  1061. u = 0;
  1062. }
  1063. var x = x1 + u * px;
  1064. var y = y1 + u * py;
  1065. var dx = x - x3;
  1066. var dy = y - y3;
  1067. //# Note: If the actual distance does not matter,
  1068. //# if you only want to compare what this function
  1069. //# returns to other results of this function, you
  1070. //# can just return the squared distance instead
  1071. //# (i.e. remove the sqrt) to gain a little performance
  1072. return Math.sqrt(dx * dx + dy * dy);
  1073. }
  1074. /**
  1075. * This allows the zoom level of the network to influence the rendering
  1076. *
  1077. * @param scale
  1078. */
  1079. setScale(scale) {
  1080. this.networkScaleInv = 1.0 / scale;
  1081. }
  1082. select() {
  1083. this.selected = true;
  1084. }
  1085. unselect() {
  1086. this.selected = false;
  1087. }
  1088. positionBezierNode() {
  1089. if (this.via !== undefined && this.from !== undefined && this.to !== undefined) {
  1090. this.via.x = 0.5 * (this.from.x + this.to.x);
  1091. this.via.y = 0.5 * (this.from.y + this.to.y);
  1092. }
  1093. else if (this.via !== undefined) {
  1094. this.via.x = 0;
  1095. this.via.y = 0;
  1096. }
  1097. }
  1098. //*************************************************************************************************//
  1099. //*************************************************************************************************//
  1100. //*************************************************************************************************//
  1101. //*************************************************************************************************//
  1102. //*********************** MOVE THESE FUNCTIONS TO THE MANIPULATION SYSTEM ************************//
  1103. //*************************************************************************************************//
  1104. //*************************************************************************************************//
  1105. //*************************************************************************************************//
  1106. //*************************************************************************************************//
  1107. /**
  1108. * This function draws the control nodes for the manipulator.
  1109. * In order to enable this, only set the this.controlNodesEnabled to true.
  1110. * @param ctx
  1111. */
  1112. _drawControlNodes(ctx) {
  1113. if (this.controlNodesEnabled == true) {
  1114. if (this.controlNodes.from === undefined && this.controlNodes.to === undefined) {
  1115. var nodeIdFrom = "edgeIdFrom:".concat(this.id);
  1116. var nodeIdTo = "edgeIdTo:".concat(this.id);
  1117. var nodeFromOptions = {
  1118. id: nodeIdFrom,
  1119. shape: 'dot',
  1120. color: {background: '#ff0000', border: '#3c3c3c', highlight: {background: '#07f968'}},
  1121. radius: 7,
  1122. borderWidth: 2,
  1123. borderWidthSelected: 2,
  1124. hidden: false,
  1125. physics: false
  1126. };
  1127. var nodeToOptions = util.deepExtend({},nodeFromOptions);
  1128. nodeToOptions.id = nodeIdTo;
  1129. this.controlNodes.from = this.body.functions.createNode(nodeFromOptions);
  1130. this.controlNodes.to = this.body.functions.createNode(nodeToOptions);
  1131. }
  1132. this.controlNodes.positions = {};
  1133. if (this.controlNodes.from.selected == false) {
  1134. this.controlNodes.positions.from = this.getControlNodeFromPosition(ctx);
  1135. this.controlNodes.from.x = this.controlNodes.positions.from.x;
  1136. this.controlNodes.from.y = this.controlNodes.positions.from.y;
  1137. }
  1138. if (this.controlNodes.to.selected == false) {
  1139. this.controlNodes.positions.to = this.getControlNodeToPosition(ctx);
  1140. this.controlNodes.to.x = this.controlNodes.positions.to.x;
  1141. this.controlNodes.to.y = this.controlNodes.positions.to.y;
  1142. }
  1143. this.controlNodes.from.draw(ctx);
  1144. this.controlNodes.to.draw(ctx);
  1145. }
  1146. else {
  1147. this.controlNodes = {from: undefined, to: undefined, positions: {}};
  1148. }
  1149. }
  1150. /**
  1151. * Enable control nodes.
  1152. * @private
  1153. */
  1154. _enableControlNodes() {
  1155. this.fromBackup = this.from;
  1156. this.toBackup = this.to;
  1157. this.controlNodesEnabled = true;
  1158. }
  1159. /**
  1160. * disable control nodes and remove from dynamicEdges from old node
  1161. * @private
  1162. */
  1163. _disableControlNodes() {
  1164. this.fromId = this.from.id;
  1165. this.toId = this.to.id;
  1166. if (this.fromId != this.fromBackup.id) { // from was changed, remove edge from old 'from' node dynamic edges
  1167. this.fromBackup.detachEdge(this);
  1168. }
  1169. else if (this.toId != this.toBackup.id) { // to was changed, remove edge from old 'to' node dynamic edges
  1170. this.toBackup.detachEdge(this);
  1171. }
  1172. this.fromBackup = undefined;
  1173. this.toBackup = undefined;
  1174. this.controlNodesEnabled = false;
  1175. }
  1176. /**
  1177. * This checks if one of the control nodes is selected and if so, returns the control node object. Else it returns undefined.
  1178. * @param x
  1179. * @param y
  1180. * @returns {undefined}
  1181. * @private
  1182. */
  1183. _getSelectedControlNode(x, y) {
  1184. var positions = this.controlNodes.positions;
  1185. var fromDistance = Math.sqrt(Math.pow(x - positions.from.x, 2) + Math.pow(y - positions.from.y, 2));
  1186. var toDistance = Math.sqrt(Math.pow(x - positions.to.x, 2) + Math.pow(y - positions.to.y, 2));
  1187. if (fromDistance < 15) {
  1188. this.connectedNode = this.from;
  1189. this.from = this.controlNodes.from;
  1190. return this.controlNodes.from;
  1191. }
  1192. else if (toDistance < 15) {
  1193. this.connectedNode = this.to;
  1194. this.to = this.controlNodes.to;
  1195. return this.controlNodes.to;
  1196. }
  1197. else {
  1198. return undefined;
  1199. }
  1200. }
  1201. /**
  1202. * this resets the control nodes to their original position.
  1203. * @private
  1204. */
  1205. _restoreControlNodes() {
  1206. if (this.controlNodes.from.selected == true) {
  1207. this.from = this.connectedNode;
  1208. this.connectedNode = undefined;
  1209. this.controlNodes.from.unselect();
  1210. }
  1211. else if (this.controlNodes.to.selected == true) {
  1212. this.to = this.connectedNode;
  1213. this.connectedNode = undefined;
  1214. this.controlNodes.to.unselect();
  1215. }
  1216. }
  1217. /**
  1218. * this calculates the position of the control nodes on the edges of the parent nodes.
  1219. *
  1220. * @param ctx
  1221. * @returns {x: *, y: *}
  1222. */
  1223. getControlNodeFromPosition(ctx) {
  1224. // draw arrow head
  1225. var controlnodeFromPos;
  1226. if (this.options.smooth.enabled == true) {
  1227. controlnodeFromPos = this._findBorderPositionBezier(true, ctx);
  1228. }
  1229. else {
  1230. var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  1231. var dx = (this.to.x - this.from.x);
  1232. var dy = (this.to.y - this.from.y);
  1233. var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
  1234. var fromBorderDist = this.from.distanceToBorder(ctx, angle + Math.PI);
  1235. var fromBorderPoint = (edgeSegmentLength - fromBorderDist) / edgeSegmentLength;
  1236. controlnodeFromPos = {};
  1237. controlnodeFromPos.x = (fromBorderPoint) * this.from.x + (1 - fromBorderPoint) * this.to.x;
  1238. controlnodeFromPos.y = (fromBorderPoint) * this.from.y + (1 - fromBorderPoint) * this.to.y;
  1239. }
  1240. return controlnodeFromPos;
  1241. }
  1242. /**
  1243. * this calculates the position of the control nodes on the edges of the parent nodes.
  1244. *
  1245. * @param ctx
  1246. * @returns {{from: {x: number, y: number}, to: {x: *, y: *}}}
  1247. */
  1248. getControlNodeToPosition(ctx) {
  1249. // draw arrow head
  1250. var controlnodeToPos;
  1251. if (this.options.smooth.enabled == true) {
  1252. controlnodeToPos = this._findBorderPositionBezier(false, ctx);
  1253. }
  1254. else {
  1255. var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  1256. var dx = (this.to.x - this.from.x);
  1257. var dy = (this.to.y - this.from.y);
  1258. var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
  1259. var toBorderDist = this.to.distanceToBorder(ctx, angle);
  1260. var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength;
  1261. controlnodeToPos = {};
  1262. controlnodeToPos.x = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x;
  1263. controlnodeToPos.y = (1 - toBorderPoint) * this.from.y + toBorderPoint * this.to.y;
  1264. }
  1265. return controlnodeToPos;
  1266. }
  1267. }
  1268. export default Edge;