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.

643 lines
18 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);
  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) {
  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. return this.edgeType.setOptions(this.options);
  120. }
  121. updateEdgeType() {
  122. let dataChanged = false;
  123. let changeInType = true;
  124. if (this.edgeType !== undefined) {
  125. if (this.edgeType instanceof BezierEdgeDynamic && this.options.smooth.enabled == true && this.options.smooth.dynamic == true) {changeInType = false;}
  126. if (this.edgeType instanceof BezierEdgeStatic && this.options.smooth.enabled == true && this.options.smooth.dynamic == false){changeInType = false;}
  127. if (this.edgeType instanceof StraightEdge && this.options.smooth.enabled == false) {changeInType = false;}
  128. if (changeInType == true) {
  129. dataChanged = this.edgeType.cleanup();
  130. }
  131. }
  132. if (changeInType === true) {
  133. if (this.options.smooth.enabled === true) {
  134. if (this.options.smooth.dynamic === true) {
  135. dataChanged = true;
  136. this.edgeType = new BezierEdgeDynamic(this.options, this.body, this.labelModule);
  137. }
  138. else {
  139. this.edgeType = new BezierEdgeStatic(this.options, this.body, this.labelModule);
  140. }
  141. }
  142. else {
  143. this.edgeType = new StraightEdge(this.options, this.body, this.labelModule);
  144. }
  145. }
  146. return dataChanged;
  147. }
  148. /**
  149. * Enable or disable the physics.
  150. * @param status
  151. */
  152. togglePhysics(status) {
  153. if (this.options.smooth.enabled == true && this.options.smooth.dynamic == true) {
  154. if (this.via === undefined) {
  155. this.via.pptions.physics = status;
  156. }
  157. }
  158. this.options.physics = status;
  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.edgeType.drawLine(ctx, this.selected, this.hover);
  243. this.drawArrows(ctx, via);
  244. this.drawLabel (ctx, via);
  245. }
  246. drawArrows(ctx, viaNode) {
  247. if (this.options.arrows.from.enabled === true) {this.edgeType.drawArrowHead(ctx,'from', viaNode);}
  248. if (this.options.arrows.middle.enabled === true) {this.edgeType.drawArrowHead(ctx,'middle', viaNode);}
  249. if (this.options.arrows.to.enabled === true) {this.edgeType.drawArrowHead(ctx,'to', viaNode);}
  250. }
  251. drawLabel(ctx, viaNode) {
  252. if (this.options.label !== undefined) {
  253. // set style
  254. var node1 = this.from;
  255. var node2 = this.to;
  256. var selected = (this.from.selected || this.to.selected || this.selected);
  257. if (node1.id != node2.id) {
  258. var point = this.edgeType.getPoint(0.5, viaNode);
  259. ctx.save();
  260. // if the label has to be rotated:
  261. if (this.options.font.align !== "horizontal") {
  262. this.labelModule.calculateLabelSize(ctx,selected,point.x,point.y);
  263. ctx.translate(point.x, this.labelModule.size.yLine);
  264. this._rotateForLabelAlignment(ctx);
  265. }
  266. // draw the label
  267. this.labelModule.draw(ctx, point.x, point.y, selected);
  268. ctx.restore();
  269. }
  270. else {
  271. var x, y;
  272. var radius = this.options.selfReferenceSize;
  273. if (node1.width > node1.height) {
  274. x = node1.x + node1.width * 0.5;
  275. y = node1.y - radius;
  276. }
  277. else {
  278. x = node1.x + radius;
  279. y = node1.y - node1.height * 0.5;
  280. }
  281. point = this._pointOnCircle(x, y, radius, 0.125);
  282. this.labelModule.draw(ctx, point.x, point.y, selected);
  283. }
  284. }
  285. }
  286. /**
  287. * Check if this object is overlapping with the provided object
  288. * @param {Object} obj an object with parameters left, top
  289. * @return {boolean} True if location is located on the edge
  290. */
  291. isOverlappingWith(obj) {
  292. if (this.connected) {
  293. var distMax = 10;
  294. var xFrom = this.from.x;
  295. var yFrom = this.from.y;
  296. var xTo = this.to.x;
  297. var yTo = this.to.y;
  298. var xObj = obj.left;
  299. var yObj = obj.top;
  300. var dist = this.edgeType.getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj);
  301. return (dist < distMax);
  302. }
  303. else {
  304. return false
  305. }
  306. }
  307. /**
  308. * Rotates the canvas so the text is most readable
  309. * @param {CanvasRenderingContext2D} ctx
  310. * @private
  311. */
  312. _rotateForLabelAlignment(ctx) {
  313. var dy = this.from.y - this.to.y;
  314. var dx = this.from.x - this.to.x;
  315. var angleInDegrees = Math.atan2(dy, dx);
  316. // rotate so label it is readable
  317. if ((angleInDegrees < -1 && dx < 0) || (angleInDegrees > 0 && dx < 0)) {
  318. angleInDegrees = angleInDegrees + Math.PI;
  319. }
  320. ctx.rotate(angleInDegrees);
  321. }
  322. /**
  323. * Get a point on a circle
  324. * @param {Number} x
  325. * @param {Number} y
  326. * @param {Number} radius
  327. * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
  328. * @return {Object} point
  329. * @private
  330. */
  331. _pointOnCircle(x, y, radius, percentage) {
  332. var angle = percentage * 2 * Math.PI;
  333. return {
  334. x: x + radius * Math.cos(angle),
  335. y: y - radius * Math.sin(angle)
  336. }
  337. }
  338. select() {
  339. this.selected = true;
  340. }
  341. unselect() {
  342. this.selected = false;
  343. }
  344. //*************************************************************************************************//
  345. //*************************************************************************************************//
  346. //*************************************************************************************************//
  347. //*************************************************************************************************//
  348. //*********************** MOVE THESE FUNCTIONS TO THE MANIPULATION SYSTEM ************************//
  349. //*************************************************************************************************//
  350. //*************************************************************************************************//
  351. //*************************************************************************************************//
  352. //*************************************************************************************************//
  353. /**
  354. * This function draws the control nodes for the manipulator.
  355. * In order to enable this, only set the this.controlNodesEnabled to true.
  356. * @param ctx
  357. */
  358. _drawControlNodes(ctx) {
  359. if (this.controlNodesEnabled == true) {
  360. if (this.controlNodes.from === undefined && this.controlNodes.to === undefined) {
  361. var nodeIdFrom = "edgeIdFrom:".concat(this.id);
  362. var nodeIdTo = "edgeIdTo:".concat(this.id);
  363. var nodeFromOptions = {
  364. id: nodeIdFrom,
  365. shape: 'dot',
  366. color: {background: '#ff0000', border: '#3c3c3c', highlight: {background: '#07f968'}},
  367. radius: 7,
  368. borderWidth: 2,
  369. borderWidthSelected: 2,
  370. hidden: false,
  371. physics: false
  372. };
  373. var nodeToOptions = util.deepExtend({},nodeFromOptions);
  374. nodeToOptions.id = nodeIdTo;
  375. this.controlNodes.from = this.body.functions.createNode(nodeFromOptions);
  376. this.controlNodes.to = this.body.functions.createNode(nodeToOptions);
  377. }
  378. this.controlNodes.positions = {};
  379. if (this.controlNodes.from.selected == false) {
  380. this.controlNodes.positions.from = this.getControlNodeFromPosition(ctx);
  381. this.controlNodes.from.x = this.controlNodes.positions.from.x;
  382. this.controlNodes.from.y = this.controlNodes.positions.from.y;
  383. }
  384. if (this.controlNodes.to.selected == false) {
  385. this.controlNodes.positions.to = this.getControlNodeToPosition(ctx);
  386. this.controlNodes.to.x = this.controlNodes.positions.to.x;
  387. this.controlNodes.to.y = this.controlNodes.positions.to.y;
  388. }
  389. this.controlNodes.from.draw(ctx);
  390. this.controlNodes.to.draw(ctx);
  391. }
  392. else {
  393. this.controlNodes = {from: undefined, to: undefined, positions: {}};
  394. }
  395. }
  396. /**
  397. * Enable control nodes.
  398. * @private
  399. */
  400. _enableControlNodes() {
  401. this.fromBackup = this.from;
  402. this.toBackup = this.to;
  403. this.controlNodesEnabled = true;
  404. }
  405. /**
  406. * disable control nodes and remove from dynamicEdges from old node
  407. * @private
  408. */
  409. _disableControlNodes() {
  410. this.fromId = this.from.id;
  411. this.toId = this.to.id;
  412. if (this.fromId != this.fromBackup.id) { // from was changed, remove edge from old 'from' node dynamic edges
  413. this.fromBackup.detachEdge(this);
  414. }
  415. else if (this.toId != this.toBackup.id) { // to was changed, remove edge from old 'to' node dynamic edges
  416. this.toBackup.detachEdge(this);
  417. }
  418. this.fromBackup = undefined;
  419. this.toBackup = undefined;
  420. this.controlNodesEnabled = false;
  421. }
  422. /**
  423. * This checks if one of the control nodes is selected and if so, returns the control node object. Else it returns undefined.
  424. * @param x
  425. * @param y
  426. * @returns {undefined}
  427. * @private
  428. */
  429. _getSelectedControlNode(x, y) {
  430. var positions = this.controlNodes.positions;
  431. var fromDistance = Math.sqrt(Math.pow(x - positions.from.x, 2) + Math.pow(y - positions.from.y, 2));
  432. var toDistance = Math.sqrt(Math.pow(x - positions.to.x, 2) + Math.pow(y - positions.to.y, 2));
  433. if (fromDistance < 15) {
  434. this.connectedNode = this.from;
  435. this.from = this.controlNodes.from;
  436. return this.controlNodes.from;
  437. }
  438. else if (toDistance < 15) {
  439. this.connectedNode = this.to;
  440. this.to = this.controlNodes.to;
  441. return this.controlNodes.to;
  442. }
  443. else {
  444. return undefined;
  445. }
  446. }
  447. /**
  448. * this resets the control nodes to their original position.
  449. * @private
  450. */
  451. _restoreControlNodes() {
  452. if (this.controlNodes.from.selected == true) {
  453. this.from = this.connectedNode;
  454. this.connectedNode = undefined;
  455. this.controlNodes.from.unselect();
  456. }
  457. else if (this.controlNodes.to.selected == true) {
  458. this.to = this.connectedNode;
  459. this.connectedNode = undefined;
  460. this.controlNodes.to.unselect();
  461. }
  462. }
  463. /**
  464. * this calculates the position of the control nodes on the edges of the parent nodes.
  465. *
  466. * @param ctx
  467. * @returns {x: *, y: *}
  468. */
  469. getControlNodeFromPosition(ctx) {
  470. // draw arrow head
  471. var controlnodeFromPos;
  472. if (this.options.smooth.enabled == true) {
  473. controlnodeFromPos = this._findBorderPositionBezier(true, ctx);
  474. }
  475. else {
  476. var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  477. var dx = (this.to.x - this.from.x);
  478. var dy = (this.to.y - this.from.y);
  479. var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
  480. var fromBorderDist = this.from.distanceToBorder(ctx, angle + Math.PI);
  481. var fromBorderPoint = (edgeSegmentLength - fromBorderDist) / edgeSegmentLength;
  482. controlnodeFromPos = {};
  483. controlnodeFromPos.x = (fromBorderPoint) * this.from.x + (1 - fromBorderPoint) * this.to.x;
  484. controlnodeFromPos.y = (fromBorderPoint) * this.from.y + (1 - fromBorderPoint) * this.to.y;
  485. }
  486. return controlnodeFromPos;
  487. }
  488. /**
  489. * this calculates the position of the control nodes on the edges of the parent nodes.
  490. *
  491. * @param ctx
  492. * @returns {{from: {x: number, y: number}, to: {x: *, y: *}}}
  493. */
  494. getControlNodeToPosition(ctx) {
  495. // draw arrow head
  496. var controlnodeToPos;
  497. if (this.options.smooth.enabled == true) {
  498. controlnodeToPos = this._findBorderPositionBezier(false, ctx);
  499. }
  500. else {
  501. var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  502. var dx = (this.to.x - this.from.x);
  503. var dy = (this.to.y - this.from.y);
  504. var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
  505. var toBorderDist = this.to.distanceToBorder(ctx, angle);
  506. var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength;
  507. controlnodeToPos = {};
  508. controlnodeToPos.x = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x;
  509. controlnodeToPos.y = (1 - toBorderPoint) * this.from.y + toBorderPoint * this.to.y;
  510. }
  511. return controlnodeToPos;
  512. }
  513. }
  514. export default Edge;