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.

746 lines
20 KiB

9 years ago
  1. var util = require('../../../util');
  2. import Label from './unified/label.js'
  3. import BezierEdgeDynamic from './edges/bezierEdgeDynamic'
  4. import BezierEdgeStatic from './edges/bezierEdgeStatic'
  5. import StraightEdge from './edges/straightEdge'
  6. /**
  7. * @class Edge
  8. *
  9. * A edge connects two nodes
  10. * @param {Object} properties Object with options. Must contain
  11. * At least options from and to.
  12. * Available options: from (number),
  13. * to (number), label (string, color (string),
  14. * width (number), style (string),
  15. * length (number), title (string)
  16. * @param {Network} network A Network object, used to find and edge to
  17. * nodes.
  18. * @param {Object} constants An object with default values for
  19. * example for the color
  20. */
  21. class Edge {
  22. constructor(options, body, globalOptions) {
  23. if (body === undefined) {
  24. throw "No body provided";
  25. }
  26. this.options = util.bridgeObject(globalOptions);
  27. this.body = body;
  28. // initialize variables
  29. this.id = undefined;
  30. this.fromId = undefined;
  31. this.toId = undefined;
  32. this.title = undefined;
  33. this.value = undefined;
  34. this.selected = false;
  35. this.hover = false;
  36. this.labelDirty = true;
  37. this.colorDirty = true;
  38. this.from = undefined; // a node
  39. this.to = undefined; // a node
  40. this.edgeType = undefined;
  41. this.connected = false;
  42. this.labelModule = new Label(this.body, this.options);
  43. this.setOptions(options, true);
  44. this.controlNodesEnabled = false;
  45. this.controlNodes = {from: undefined, to: undefined, positions: {}};
  46. this.connectedNode = undefined;
  47. }
  48. /**
  49. * Set or overwrite options for the edge
  50. * @param {Object} options an object with options
  51. * @param doNotEmit
  52. */
  53. setOptions(options, doNotEmit = false) {
  54. if (!options) {
  55. return;
  56. }
  57. this.colorDirty = true;
  58. var fields = [
  59. 'id',
  60. 'font',
  61. 'from',
  62. 'hidden',
  63. 'hoverWidth',
  64. 'label',
  65. 'length',
  66. 'line',
  67. 'opacity',
  68. 'physics',
  69. 'scaling',
  70. 'selfReferenceSize',
  71. 'to',
  72. 'value',
  73. 'width',
  74. 'widthMin',
  75. 'widthMax',
  76. 'widthSelectionMultiplier'
  77. ];
  78. util.selectiveDeepExtend(fields, this.options, options);
  79. util.mergeOptions(this.options, options, 'smooth');
  80. util.mergeOptions(this.options, options, 'dashes');
  81. if (options.arrows !== undefined) {
  82. util.mergeOptions(this.options.arrows, options.arrows, 'to');
  83. util.mergeOptions(this.options.arrows, options.arrows, 'middle');
  84. util.mergeOptions(this.options.arrows, options.arrows, 'from');
  85. }
  86. if (options.id !== undefined) {this.id = options.id;}
  87. if (options.from !== undefined) {this.fromId = options.from;}
  88. if (options.to !== undefined) {this.toId = options.to;}
  89. if (options.title !== undefined) {this.title = options.title;}
  90. if (options.value !== undefined) {this.value = options.value;}
  91. if (options.color !== undefined) {
  92. if (util.isString(options.color)) {
  93. this.options.color.color = options.color;
  94. this.options.color.highlight = options.color;
  95. }
  96. else {
  97. if (options.color.color !== undefined) {
  98. this.options.color.color = options.color.color;
  99. }
  100. if (options.color.highlight !== undefined) {
  101. this.options.color.highlight = options.color.highlight;
  102. }
  103. if (options.color.hover !== undefined) {
  104. this.options.color.hover = options.color.hover;
  105. }
  106. }
  107. // inherit colors
  108. if (options.color.inherit === undefined) {
  109. this.options.color.inherit.enabled = false;
  110. }
  111. else {
  112. util.mergeOptions(this.options.color, options.color, 'inherit');
  113. }
  114. }
  115. // A node is connected when it has a from and to node that both exist in the network.body.nodes.
  116. this.connect();
  117. this.labelModule.setOptions(this.options);
  118. this.updateEdgeType();
  119. this.edgeType.setOptions(this.options);
  120. }
  121. updateEdgeType() {
  122. if (this.edgeType !== undefined) {
  123. this.edgeType.cleanup();
  124. }
  125. if (this.options.smooth.enabled === true) {
  126. if (this.options.smooth.dynamic === true) {
  127. this.edgeType = new BezierEdgeDynamic(this.options, this.body, this.labelModule);
  128. }
  129. else {
  130. this.edgeType = new BezierEdgeStatic(this.options, this.body, this.labelModule);
  131. }
  132. }
  133. else {
  134. this.edgeType = new StraightEdge(this.options, this.body, this.labelModule);
  135. }
  136. }
  137. /**
  138. * Enable or disable the physics.
  139. * @param status
  140. */
  141. togglePhysics(status) {
  142. if (this.options.smooth.enabled == true && this.options.smooth.dynamic == true) {
  143. if (this.via === undefined) {
  144. this.via.pptions.physics = status;
  145. }
  146. }
  147. this.options.physics = status;
  148. }
  149. /**
  150. * Connect an edge to its nodes
  151. */
  152. connect() {
  153. this.disconnect();
  154. this.from = this.body.nodes[this.fromId] || undefined;
  155. this.to = this.body.nodes[this.toId] || undefined;
  156. this.connected = (this.from !== undefined && this.to !== undefined);
  157. if (this.connected === true) {
  158. this.from.attachEdge(this);
  159. this.to.attachEdge(this);
  160. }
  161. else {
  162. if (this.from) {
  163. this.from.detachEdge(this);
  164. }
  165. if (this.to) {
  166. this.to.detachEdge(this);
  167. }
  168. }
  169. }
  170. /**
  171. * Disconnect an edge from its nodes
  172. */
  173. disconnect() {
  174. if (this.from) {
  175. this.from.detachEdge(this);
  176. this.from = undefined;
  177. }
  178. if (this.to) {
  179. this.to.detachEdge(this);
  180. this.to = undefined;
  181. }
  182. this.connected = false;
  183. }
  184. /**
  185. * get the title of this edge.
  186. * @return {string} title The title of the edge, or undefined when no title
  187. * has been set.
  188. */
  189. getTitle() {
  190. return typeof this.title === "function" ? this.title() : this.title;
  191. }
  192. /**
  193. * check if this node is selecte
  194. * @return {boolean} selected True if node is selected, else false
  195. */
  196. isSelected() {
  197. return this.selected;
  198. }
  199. /**
  200. * Retrieve the value of the edge. Can be undefined
  201. * @return {Number} value
  202. */
  203. getValue() {
  204. return this.value;
  205. }
  206. /**
  207. * Adjust the value range of the edge. The edge will adjust it's width
  208. * based on its value.
  209. * @param {Number} min
  210. * @param {Number} max
  211. * @param total
  212. */
  213. setValueRange(min, max, total) {
  214. if (this.value !== undefined) {
  215. var scale = this.options.scaling.customScalingFunction(min, max, total, this.value);
  216. var widthDiff = this.options.scaling.max - this.options.scaling.min;
  217. if (this.options.scaling.label.enabled == true) {
  218. var fontDiff = this.options.scaling.label.max - this.options.scaling.label.min;
  219. this.options.font.size = this.options.scaling.label.min + scale * fontDiff;
  220. }
  221. this.options.width = this.options.scaling.min + scale * widthDiff;
  222. }
  223. }
  224. /**
  225. * Redraw a edge
  226. * Draw this edge in the given canvas
  227. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  228. * @param {CanvasRenderingContext2D} ctx
  229. */
  230. draw(ctx) {
  231. let via = this.edgeType.drawLine(ctx, this.selected, this.hover);
  232. this.drawArrows(ctx, via);
  233. this.drawLabel (ctx, via);
  234. }
  235. drawArrows(ctx, viaNode) {
  236. if (this.options.arrows.from.enabled === true) {this._drawArrowHead(ctx,'from', viaNode);}
  237. if (this.options.arrows.middle.enabled === true) {this._drawArrowHead(ctx,'middle', viaNode);}
  238. if (this.options.arrows.to.enabled === true) {this._drawArrowHead(ctx,'to', viaNode);}
  239. }
  240. drawLabel(ctx, viaNode) {
  241. if (this.options.label !== undefined) {
  242. // set style
  243. var node1 = this.from;
  244. var node2 = this.to;
  245. var selected = (this.from.selected || this.to.selected || this.selected);
  246. if (node1.id != node2.id) {
  247. var point = this.edgeType.getPoint(0.5, viaNode);
  248. ctx.save();
  249. // if the label has to be rotated:
  250. if (this.options.font.align !== "horizontal") {
  251. this.labelModule.calculateLabelSize(ctx,selected,point.x,point.y);
  252. ctx.translate(point.x, this.labelModule.size.yLine);
  253. this._rotateForLabelAlignment(ctx);
  254. }
  255. // draw the label
  256. this.labelModule.draw(ctx, point.x, point.y, selected);
  257. ctx.restore();
  258. }
  259. else {
  260. var x, y;
  261. var radius = this.options.selfReferenceSize;
  262. if (node1.width > node1.height) {
  263. x = node1.x + node1.width * 0.5;
  264. y = node1.y - radius;
  265. }
  266. else {
  267. x = node1.x + radius;
  268. y = node1.y - node1.height * 0.5;
  269. }
  270. point = this._pointOnCircle(x, y, radius, 0.125);
  271. this.labelModule.draw(ctx, point.x, point.y, selected);
  272. }
  273. }
  274. }
  275. /**
  276. * Check if this object is overlapping with the provided object
  277. * @param {Object} obj an object with parameters left, top
  278. * @return {boolean} True if location is located on the edge
  279. */
  280. isOverlappingWith(obj) {
  281. if (this.connected) {
  282. var distMax = 10;
  283. var xFrom = this.from.x;
  284. var yFrom = this.from.y;
  285. var xTo = this.to.x;
  286. var yTo = this.to.y;
  287. var xObj = obj.left;
  288. var yObj = obj.top;
  289. var dist = this.edgeType.getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj);
  290. return (dist < distMax);
  291. }
  292. else {
  293. return false
  294. }
  295. }
  296. /**
  297. * Rotates the canvas so the text is most readable
  298. * @param {CanvasRenderingContext2D} ctx
  299. * @private
  300. */
  301. _rotateForLabelAlignment(ctx) {
  302. var dy = this.from.y - this.to.y;
  303. var dx = this.from.x - this.to.x;
  304. var angleInDegrees = Math.atan2(dy, dx);
  305. // rotate so label it is readable
  306. if ((angleInDegrees < -1 && dx < 0) || (angleInDegrees > 0 && dx < 0)) {
  307. angleInDegrees = angleInDegrees + Math.PI;
  308. }
  309. ctx.rotate(angleInDegrees);
  310. }
  311. /**
  312. * Get a point on a circle
  313. * @param {Number} x
  314. * @param {Number} y
  315. * @param {Number} radius
  316. * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
  317. * @return {Object} point
  318. * @private
  319. */
  320. _pointOnCircle(x, y, radius, percentage) {
  321. var angle = percentage * 2 * Math.PI;
  322. return {
  323. x: x + radius * Math.cos(angle),
  324. y: y - radius * Math.sin(angle)
  325. }
  326. }
  327. /**
  328. *
  329. * @param ctx
  330. * @param position
  331. * @param viaNode
  332. */
  333. _drawArrowHead(ctx,position,viaNode) {
  334. // set style
  335. ctx.strokeStyle = this.edgeType.getColor(ctx);
  336. ctx.fillStyle = ctx.strokeStyle;
  337. ctx.lineWidth = this.edgeType.getLineWidth();
  338. // set lets
  339. let angle;
  340. let length;
  341. let arrowPos;
  342. let node1;
  343. let node2;
  344. let guideOffset;
  345. let scaleFactor;
  346. if (position == 'from') {
  347. node1 = this.from;
  348. node2 = this.to;
  349. guideOffset = 0.1;
  350. scaleFactor = this.options.arrows.from.scaleFactor;
  351. }
  352. else if (position == 'to') {
  353. node1 = this.to;
  354. node2 = this.from;
  355. guideOffset = -0.1;
  356. scaleFactor = this.options.arrows.to.scaleFactor;
  357. }
  358. else {
  359. node1 = this.to;
  360. node2 = this.from;
  361. scaleFactor = this.options.arrows.middle.scaleFactor;
  362. }
  363. // if not connected to itself
  364. if (node1 != node2) {
  365. if (position !== 'middle') {
  366. // draw arrow head
  367. if (this.options.smooth.enabled == true) {
  368. arrowPos = this.edgeType.findBorderPosition(node1, ctx, {via:viaNode});
  369. let guidePos = this.edgeType.getPoint(Math.max(0.0,Math.min(1.0,arrowPos.t + guideOffset)), viaNode);
  370. angle = Math.atan2((arrowPos.y - guidePos.y), (arrowPos.x - guidePos.x));
  371. }
  372. else {
  373. angle = Math.atan2((node1.y - node2.y), (node1.x - node2.x));
  374. arrowPos = this.edgeType.findBorderPosition(node1, ctx);
  375. }
  376. }
  377. else {
  378. angle = Math.atan2((node1.y - node2.y), (node1.x - node2.x));
  379. arrowPos = this.edgeType.getPoint(0.6, viaNode); // this is 0.6 to account for the size of the arrow.
  380. }
  381. // draw arrow at the end of the line
  382. length = (10 + 5 * this.options.width) * scaleFactor;
  383. ctx.arrow(arrowPos.x, arrowPos.y, angle, length);
  384. ctx.fill();
  385. ctx.stroke();
  386. }
  387. else {
  388. // draw circle
  389. let angle, point;
  390. let x, y;
  391. let radius = this.options.selfReferenceSize;
  392. if (!node1.width) {
  393. node1.resize(ctx);
  394. }
  395. // get circle coordinates
  396. if (node1.width > node1.height) {
  397. x = node1.x + node1.width * 0.5;
  398. y = node1.y - radius;
  399. }
  400. else {
  401. x = node1.x + radius;
  402. y = node1.y - node1.height * 0.5;
  403. }
  404. if (position == 'from') {
  405. point = this.edgeType.findBorderPosition(x, y, radius, node1, 0.25, 0.6, -1, ctx);
  406. angle = point.t * -2 * Math.PI + 1.5 * Math.PI + 0.1 * Math.PI;
  407. }
  408. else if (position == 'to') {
  409. point = this.edgeType.findBorderPosition(x, y, radius, node1, 0.6, 0.8, 1, ctx);
  410. angle = point.t * -2 * Math.PI + 1.5 * Math.PI - 1.1 * Math.PI;
  411. }
  412. else {
  413. point = this.edgeType.findBorderPosition(x,y,radius,0.175);
  414. angle = 3.9269908169872414; // == 0.175 * -2 * Math.PI + 1.5 * Math.PI + 0.1 * Math.PI;
  415. }
  416. // draw the arrowhead
  417. let length = (10 + 5 * this.options.width) * scaleFactor;
  418. ctx.arrow(point.x, point.y, angle, length);
  419. ctx.fill();
  420. ctx.stroke();
  421. }
  422. }
  423. /**
  424. * This allows the zoom level of the network to influence the rendering
  425. *
  426. * @param scale
  427. */
  428. setScale(scale) {
  429. this.networkScaleInv = 1.0 / scale;
  430. }
  431. select() {
  432. this.selected = true;
  433. }
  434. unselect() {
  435. this.selected = false;
  436. }
  437. //*************************************************************************************************//
  438. //*************************************************************************************************//
  439. //*************************************************************************************************//
  440. //*************************************************************************************************//
  441. //*********************** MOVE THESE FUNCTIONS TO THE MANIPULATION SYSTEM ************************//
  442. //*************************************************************************************************//
  443. //*************************************************************************************************//
  444. //*************************************************************************************************//
  445. //*************************************************************************************************//
  446. /**
  447. * This function draws the control nodes for the manipulator.
  448. * In order to enable this, only set the this.controlNodesEnabled to true.
  449. * @param ctx
  450. */
  451. _drawControlNodes(ctx) {
  452. if (this.controlNodesEnabled == true) {
  453. if (this.controlNodes.from === undefined && this.controlNodes.to === undefined) {
  454. var nodeIdFrom = "edgeIdFrom:".concat(this.id);
  455. var nodeIdTo = "edgeIdTo:".concat(this.id);
  456. var nodeFromOptions = {
  457. id: nodeIdFrom,
  458. shape: 'dot',
  459. color: {background: '#ff0000', border: '#3c3c3c', highlight: {background: '#07f968'}},
  460. radius: 7,
  461. borderWidth: 2,
  462. borderWidthSelected: 2,
  463. hidden: false,
  464. physics: false
  465. };
  466. var nodeToOptions = util.deepExtend({},nodeFromOptions);
  467. nodeToOptions.id = nodeIdTo;
  468. this.controlNodes.from = this.body.functions.createNode(nodeFromOptions);
  469. this.controlNodes.to = this.body.functions.createNode(nodeToOptions);
  470. }
  471. this.controlNodes.positions = {};
  472. if (this.controlNodes.from.selected == false) {
  473. this.controlNodes.positions.from = this.getControlNodeFromPosition(ctx);
  474. this.controlNodes.from.x = this.controlNodes.positions.from.x;
  475. this.controlNodes.from.y = this.controlNodes.positions.from.y;
  476. }
  477. if (this.controlNodes.to.selected == false) {
  478. this.controlNodes.positions.to = this.getControlNodeToPosition(ctx);
  479. this.controlNodes.to.x = this.controlNodes.positions.to.x;
  480. this.controlNodes.to.y = this.controlNodes.positions.to.y;
  481. }
  482. this.controlNodes.from.draw(ctx);
  483. this.controlNodes.to.draw(ctx);
  484. }
  485. else {
  486. this.controlNodes = {from: undefined, to: undefined, positions: {}};
  487. }
  488. }
  489. /**
  490. * Enable control nodes.
  491. * @private
  492. */
  493. _enableControlNodes() {
  494. this.fromBackup = this.from;
  495. this.toBackup = this.to;
  496. this.controlNodesEnabled = true;
  497. }
  498. /**
  499. * disable control nodes and remove from dynamicEdges from old node
  500. * @private
  501. */
  502. _disableControlNodes() {
  503. this.fromId = this.from.id;
  504. this.toId = this.to.id;
  505. if (this.fromId != this.fromBackup.id) { // from was changed, remove edge from old 'from' node dynamic edges
  506. this.fromBackup.detachEdge(this);
  507. }
  508. else if (this.toId != this.toBackup.id) { // to was changed, remove edge from old 'to' node dynamic edges
  509. this.toBackup.detachEdge(this);
  510. }
  511. this.fromBackup = undefined;
  512. this.toBackup = undefined;
  513. this.controlNodesEnabled = false;
  514. }
  515. /**
  516. * This checks if one of the control nodes is selected and if so, returns the control node object. Else it returns undefined.
  517. * @param x
  518. * @param y
  519. * @returns {undefined}
  520. * @private
  521. */
  522. _getSelectedControlNode(x, y) {
  523. var positions = this.controlNodes.positions;
  524. var fromDistance = Math.sqrt(Math.pow(x - positions.from.x, 2) + Math.pow(y - positions.from.y, 2));
  525. var toDistance = Math.sqrt(Math.pow(x - positions.to.x, 2) + Math.pow(y - positions.to.y, 2));
  526. if (fromDistance < 15) {
  527. this.connectedNode = this.from;
  528. this.from = this.controlNodes.from;
  529. return this.controlNodes.from;
  530. }
  531. else if (toDistance < 15) {
  532. this.connectedNode = this.to;
  533. this.to = this.controlNodes.to;
  534. return this.controlNodes.to;
  535. }
  536. else {
  537. return undefined;
  538. }
  539. }
  540. /**
  541. * this resets the control nodes to their original position.
  542. * @private
  543. */
  544. _restoreControlNodes() {
  545. if (this.controlNodes.from.selected == true) {
  546. this.from = this.connectedNode;
  547. this.connectedNode = undefined;
  548. this.controlNodes.from.unselect();
  549. }
  550. else if (this.controlNodes.to.selected == true) {
  551. this.to = this.connectedNode;
  552. this.connectedNode = undefined;
  553. this.controlNodes.to.unselect();
  554. }
  555. }
  556. /**
  557. * this calculates the position of the control nodes on the edges of the parent nodes.
  558. *
  559. * @param ctx
  560. * @returns {x: *, y: *}
  561. */
  562. getControlNodeFromPosition(ctx) {
  563. // draw arrow head
  564. var controlnodeFromPos;
  565. if (this.options.smooth.enabled == true) {
  566. controlnodeFromPos = this._findBorderPositionBezier(true, ctx);
  567. }
  568. else {
  569. var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  570. var dx = (this.to.x - this.from.x);
  571. var dy = (this.to.y - this.from.y);
  572. var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
  573. var fromBorderDist = this.from.distanceToBorder(ctx, angle + Math.PI);
  574. var fromBorderPoint = (edgeSegmentLength - fromBorderDist) / edgeSegmentLength;
  575. controlnodeFromPos = {};
  576. controlnodeFromPos.x = (fromBorderPoint) * this.from.x + (1 - fromBorderPoint) * this.to.x;
  577. controlnodeFromPos.y = (fromBorderPoint) * this.from.y + (1 - fromBorderPoint) * this.to.y;
  578. }
  579. return controlnodeFromPos;
  580. }
  581. /**
  582. * this calculates the position of the control nodes on the edges of the parent nodes.
  583. *
  584. * @param ctx
  585. * @returns {{from: {x: number, y: number}, to: {x: *, y: *}}}
  586. */
  587. getControlNodeToPosition(ctx) {
  588. // draw arrow head
  589. var controlnodeToPos;
  590. if (this.options.smooth.enabled == true) {
  591. controlnodeToPos = this._findBorderPositionBezier(false, ctx);
  592. }
  593. else {
  594. var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  595. var dx = (this.to.x - this.from.x);
  596. var dy = (this.to.y - this.from.y);
  597. var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
  598. var toBorderDist = this.to.distanceToBorder(ctx, angle);
  599. var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength;
  600. controlnodeToPos = {};
  601. controlnodeToPos.x = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x;
  602. controlnodeToPos.y = (1 - toBorderPoint) * this.from.y + toBorderPoint * this.to.y;
  603. }
  604. return controlnodeToPos;
  605. }
  606. }
  607. export default Edge;