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.

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