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.

663 lines
19 KiB

10 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. if (typeof options.arrows === 'string') {
  83. let arrows = options.arrows.toLowerCase();
  84. if (arrows.indexOf("to") != -1) {this.options.arrows.to.enabled = true;}
  85. if (arrows.indexOf("middle") != -1) {this.options.arrows.middle.enabled = true;}
  86. if (arrows.indexOf("from") != -1) {this.options.arrows.from.enabled = true;}
  87. }
  88. else if (typeof options.arrows === 'object') {
  89. util.mergeOptions(this.options.arrows, options.arrows, 'to');
  90. util.mergeOptions(this.options.arrows, options.arrows, 'middle');
  91. util.mergeOptions(this.options.arrows, options.arrows, 'from');
  92. }
  93. else {
  94. throw new Error("The arrow options can only be an object or a string. Refer to the documentation. You used:" + JSON.stringify(options.arrows));
  95. }
  96. }
  97. if (options.id !== undefined) {this.id = options.id;}
  98. if (options.from !== undefined) {this.fromId = options.from;}
  99. if (options.to !== undefined) {this.toId = options.to;}
  100. if (options.title !== undefined) {this.title = options.title;}
  101. if (options.value !== undefined) {this.value = options.value;}
  102. if (options.color !== undefined) {
  103. if (options.color !== undefined) {
  104. if (util.isString(options.color)) {
  105. util.assignAllKeys(this.options.color, options.color);
  106. }
  107. else {
  108. util.extend(this.options.color, options.color);
  109. }
  110. this.options.color.inherit.enabled = false;
  111. }
  112. //if (util.isString(options.color)) {
  113. // this.options.color.color = options.color;
  114. // this.options.color.highlight = options.color;
  115. //}
  116. //else {
  117. // if (options.color.color !== undefined) {
  118. // this.options.color.color = options.color.color;
  119. // }
  120. // if (options.color.highlight !== undefined) {
  121. // this.options.color.highlight = options.color.highlight;
  122. // }
  123. // if (options.color.hover !== undefined) {
  124. // this.options.color.hover = options.color.hover;
  125. // }
  126. //}
  127. //
  128. //// inherit colors
  129. //if (options.color.inherit === undefined) {
  130. // this.options.color.inherit.enabled = false;
  131. //}
  132. //else {
  133. // util.mergeOptions(this.options.color, options.color, 'inherit');
  134. //}
  135. }
  136. // A node is connected when it has a from and to node that both exist in the network.body.nodes.
  137. this.connect();
  138. this.labelModule.setOptions(this.options);
  139. this.updateEdgeType();
  140. return this.edgeType.setOptions(this.options);
  141. }
  142. updateEdgeType() {
  143. let dataChanged = false;
  144. let changeInType = true;
  145. if (this.edgeType !== undefined) {
  146. if (this.edgeType instanceof BezierEdgeDynamic && this.options.smooth.enabled == true && this.options.smooth.dynamic == true) {changeInType = false;}
  147. if (this.edgeType instanceof BezierEdgeStatic && this.options.smooth.enabled == true && this.options.smooth.dynamic == false){changeInType = false;}
  148. if (this.edgeType instanceof StraightEdge && this.options.smooth.enabled == false) {changeInType = false;}
  149. if (changeInType == true) {
  150. dataChanged = this.edgeType.cleanup();
  151. }
  152. }
  153. if (changeInType === true) {
  154. if (this.options.smooth.enabled === true) {
  155. if (this.options.smooth.dynamic === true) {
  156. dataChanged = true;
  157. this.edgeType = new BezierEdgeDynamic(this.options, this.body, this.labelModule);
  158. }
  159. else {
  160. this.edgeType = new BezierEdgeStatic(this.options, this.body, this.labelModule);
  161. }
  162. }
  163. else {
  164. this.edgeType = new StraightEdge(this.options, this.body, this.labelModule);
  165. }
  166. }
  167. return dataChanged;
  168. }
  169. /**
  170. * Enable or disable the physics.
  171. * @param status
  172. */
  173. togglePhysics(status) {
  174. if (this.options.smooth.enabled == true && this.options.smooth.dynamic == true) {
  175. if (this.via === undefined) {
  176. this.via.pptions.physics = status;
  177. }
  178. }
  179. this.options.physics = status;
  180. }
  181. /**
  182. * Connect an edge to its nodes
  183. */
  184. connect() {
  185. this.disconnect();
  186. this.from = this.body.nodes[this.fromId] || undefined;
  187. this.to = this.body.nodes[this.toId] || undefined;
  188. this.connected = (this.from !== undefined && this.to !== undefined);
  189. if (this.connected === true) {
  190. this.from.attachEdge(this);
  191. this.to.attachEdge(this);
  192. }
  193. else {
  194. if (this.from) {
  195. this.from.detachEdge(this);
  196. }
  197. if (this.to) {
  198. this.to.detachEdge(this);
  199. }
  200. }
  201. }
  202. /**
  203. * Disconnect an edge from its nodes
  204. */
  205. disconnect() {
  206. if (this.from) {
  207. this.from.detachEdge(this);
  208. this.from = undefined;
  209. }
  210. if (this.to) {
  211. this.to.detachEdge(this);
  212. this.to = undefined;
  213. }
  214. this.connected = false;
  215. }
  216. /**
  217. * get the title of this edge.
  218. * @return {string} title The title of the edge, or undefined when no title
  219. * has been set.
  220. */
  221. getTitle() {
  222. return typeof this.title === "function" ? this.title() : this.title;
  223. }
  224. /**
  225. * check if this node is selecte
  226. * @return {boolean} selected True if node is selected, else false
  227. */
  228. isSelected() {
  229. return this.selected;
  230. }
  231. /**
  232. * Retrieve the value of the edge. Can be undefined
  233. * @return {Number} value
  234. */
  235. getValue() {
  236. return this.value;
  237. }
  238. /**
  239. * Adjust the value range of the edge. The edge will adjust it's width
  240. * based on its value.
  241. * @param {Number} min
  242. * @param {Number} max
  243. * @param total
  244. */
  245. setValueRange(min, max, total) {
  246. if (this.value !== undefined) {
  247. var scale = this.options.scaling.customScalingFunction(min, max, total, this.value);
  248. var widthDiff = this.options.scaling.max - this.options.scaling.min;
  249. if (this.options.scaling.label.enabled == true) {
  250. var fontDiff = this.options.scaling.label.max - this.options.scaling.label.min;
  251. this.options.font.size = this.options.scaling.label.min + scale * fontDiff;
  252. }
  253. this.options.width = this.options.scaling.min + scale * widthDiff;
  254. }
  255. }
  256. /**
  257. * Redraw a edge
  258. * Draw this edge in the given canvas
  259. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  260. * @param {CanvasRenderingContext2D} ctx
  261. */
  262. draw(ctx) {
  263. let via = this.edgeType.drawLine(ctx, this.selected, this.hover);
  264. this.drawArrows(ctx, via);
  265. this.drawLabel (ctx, via);
  266. }
  267. drawArrows(ctx, viaNode) {
  268. if (this.options.arrows.from.enabled === true) {this.edgeType.drawArrowHead(ctx,'from', viaNode);}
  269. if (this.options.arrows.middle.enabled === true) {this.edgeType.drawArrowHead(ctx,'middle', viaNode);}
  270. if (this.options.arrows.to.enabled === true) {this.edgeType.drawArrowHead(ctx,'to', viaNode);}
  271. }
  272. drawLabel(ctx, viaNode) {
  273. if (this.options.label !== undefined) {
  274. // set style
  275. var node1 = this.from;
  276. var node2 = this.to;
  277. var selected = (this.from.selected || this.to.selected || this.selected);
  278. if (node1.id != node2.id) {
  279. var point = this.edgeType.getPoint(0.5, viaNode);
  280. ctx.save();
  281. // if the label has to be rotated:
  282. if (this.options.font.align !== "horizontal") {
  283. this.labelModule.calculateLabelSize(ctx,selected,point.x,point.y);
  284. ctx.translate(point.x, this.labelModule.size.yLine);
  285. this._rotateForLabelAlignment(ctx);
  286. }
  287. // draw the label
  288. this.labelModule.draw(ctx, point.x, point.y, selected);
  289. ctx.restore();
  290. }
  291. else {
  292. var x, y;
  293. var radius = this.options.selfReferenceSize;
  294. if (node1.width > node1.height) {
  295. x = node1.x + node1.width * 0.5;
  296. y = node1.y - radius;
  297. }
  298. else {
  299. x = node1.x + radius;
  300. y = node1.y - node1.height * 0.5;
  301. }
  302. point = this._pointOnCircle(x, y, radius, 0.125);
  303. this.labelModule.draw(ctx, point.x, point.y, selected);
  304. }
  305. }
  306. }
  307. /**
  308. * Check if this object is overlapping with the provided object
  309. * @param {Object} obj an object with parameters left, top
  310. * @return {boolean} True if location is located on the edge
  311. */
  312. isOverlappingWith(obj) {
  313. if (this.connected) {
  314. var distMax = 10;
  315. var xFrom = this.from.x;
  316. var yFrom = this.from.y;
  317. var xTo = this.to.x;
  318. var yTo = this.to.y;
  319. var xObj = obj.left;
  320. var yObj = obj.top;
  321. var dist = this.edgeType.getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj);
  322. return (dist < distMax);
  323. }
  324. else {
  325. return false
  326. }
  327. }
  328. /**
  329. * Rotates the canvas so the text is most readable
  330. * @param {CanvasRenderingContext2D} ctx
  331. * @private
  332. */
  333. _rotateForLabelAlignment(ctx) {
  334. var dy = this.from.y - this.to.y;
  335. var dx = this.from.x - this.to.x;
  336. var angleInDegrees = Math.atan2(dy, dx);
  337. // rotate so label it is readable
  338. if ((angleInDegrees < -1 && dx < 0) || (angleInDegrees > 0 && dx < 0)) {
  339. angleInDegrees = angleInDegrees + Math.PI;
  340. }
  341. ctx.rotate(angleInDegrees);
  342. }
  343. /**
  344. * Get a point on a circle
  345. * @param {Number} x
  346. * @param {Number} y
  347. * @param {Number} radius
  348. * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
  349. * @return {Object} point
  350. * @private
  351. */
  352. _pointOnCircle(x, y, radius, percentage) {
  353. var angle = percentage * 2 * Math.PI;
  354. return {
  355. x: x + radius * Math.cos(angle),
  356. y: y - radius * Math.sin(angle)
  357. }
  358. }
  359. select() {
  360. this.selected = true;
  361. }
  362. unselect() {
  363. this.selected = false;
  364. }
  365. //*************************************************************************************************//
  366. //*************************************************************************************************//
  367. //*************************************************************************************************//
  368. //*************************************************************************************************//
  369. //*********************** MOVE THESE FUNCTIONS TO THE MANIPULATION SYSTEM ************************//
  370. //*************************************************************************************************//
  371. //*************************************************************************************************//
  372. //*************************************************************************************************//
  373. //*************************************************************************************************//
  374. /**
  375. * This function draws the control nodes for the manipulator.
  376. * In order to enable this, only set the this.controlNodesEnabled to true.
  377. * @param ctx
  378. */
  379. _drawControlNodes(ctx) {
  380. if (this.controlNodesEnabled == true) {
  381. if (this.controlNodes.from === undefined && this.controlNodes.to === undefined) {
  382. var nodeIdFrom = "edgeIdFrom:".concat(this.id);
  383. var nodeIdTo = "edgeIdTo:".concat(this.id);
  384. var nodeFromOptions = {
  385. id: nodeIdFrom,
  386. shape: 'dot',
  387. color: {background: '#ff0000', border: '#3c3c3c', highlight: {background: '#07f968'}},
  388. radius: 7,
  389. borderWidth: 2,
  390. borderWidthSelected: 2,
  391. hidden: false,
  392. physics: false
  393. };
  394. var nodeToOptions = util.deepExtend({},nodeFromOptions);
  395. nodeToOptions.id = nodeIdTo;
  396. this.controlNodes.from = this.body.functions.createNode(nodeFromOptions);
  397. this.controlNodes.to = this.body.functions.createNode(nodeToOptions);
  398. }
  399. this.controlNodes.positions = {};
  400. if (this.controlNodes.from.selected == false) {
  401. this.controlNodes.positions.from = this.getControlNodeFromPosition(ctx);
  402. this.controlNodes.from.x = this.controlNodes.positions.from.x;
  403. this.controlNodes.from.y = this.controlNodes.positions.from.y;
  404. }
  405. if (this.controlNodes.to.selected == false) {
  406. this.controlNodes.positions.to = this.getControlNodeToPosition(ctx);
  407. this.controlNodes.to.x = this.controlNodes.positions.to.x;
  408. this.controlNodes.to.y = this.controlNodes.positions.to.y;
  409. }
  410. this.controlNodes.from.draw(ctx);
  411. this.controlNodes.to.draw(ctx);
  412. }
  413. else {
  414. this.controlNodes = {from: undefined, to: undefined, positions: {}};
  415. }
  416. }
  417. /**
  418. * Enable control nodes.
  419. * @private
  420. */
  421. _enableControlNodes() {
  422. this.fromBackup = this.from;
  423. this.toBackup = this.to;
  424. this.controlNodesEnabled = true;
  425. }
  426. /**
  427. * disable control nodes and remove from dynamicEdges from old node
  428. * @private
  429. */
  430. _disableControlNodes() {
  431. this.fromId = this.from.id;
  432. this.toId = this.to.id;
  433. if (this.fromId != this.fromBackup.id) { // from was changed, remove edge from old 'from' node dynamic edges
  434. this.fromBackup.detachEdge(this);
  435. }
  436. else if (this.toId != this.toBackup.id) { // to was changed, remove edge from old 'to' node dynamic edges
  437. this.toBackup.detachEdge(this);
  438. }
  439. this.fromBackup = undefined;
  440. this.toBackup = undefined;
  441. this.controlNodesEnabled = false;
  442. }
  443. /**
  444. * This checks if one of the control nodes is selected and if so, returns the control node object. Else it returns undefined.
  445. * @param x
  446. * @param y
  447. * @returns {undefined}
  448. * @private
  449. */
  450. _getSelectedControlNode(x, y) {
  451. var positions = this.controlNodes.positions;
  452. var fromDistance = Math.sqrt(Math.pow(x - positions.from.x, 2) + Math.pow(y - positions.from.y, 2));
  453. var toDistance = Math.sqrt(Math.pow(x - positions.to.x, 2) + Math.pow(y - positions.to.y, 2));
  454. if (fromDistance < 15) {
  455. this.connectedNode = this.from;
  456. this.from = this.controlNodes.from;
  457. return this.controlNodes.from;
  458. }
  459. else if (toDistance < 15) {
  460. this.connectedNode = this.to;
  461. this.to = this.controlNodes.to;
  462. return this.controlNodes.to;
  463. }
  464. else {
  465. return undefined;
  466. }
  467. }
  468. /**
  469. * this resets the control nodes to their original position.
  470. * @private
  471. */
  472. _restoreControlNodes() {
  473. if (this.controlNodes.from.selected == true) {
  474. this.from = this.connectedNode;
  475. this.connectedNode = undefined;
  476. this.controlNodes.from.unselect();
  477. }
  478. else if (this.controlNodes.to.selected == true) {
  479. this.to = this.connectedNode;
  480. this.connectedNode = undefined;
  481. this.controlNodes.to.unselect();
  482. }
  483. }
  484. /**
  485. * this calculates the position of the control nodes on the edges of the parent nodes.
  486. *
  487. * @param ctx
  488. * @returns {x: *, y: *}
  489. */
  490. getControlNodeFromPosition(ctx) {
  491. // draw arrow head
  492. var controlnodeFromPos;
  493. if (this.options.smooth.enabled == true) {
  494. controlnodeFromPos = this._findBorderPositionBezier(true, ctx);
  495. }
  496. else {
  497. var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  498. var dx = (this.to.x - this.from.x);
  499. var dy = (this.to.y - this.from.y);
  500. var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
  501. var fromBorderDist = this.from.distanceToBorder(ctx, angle + Math.PI);
  502. var fromBorderPoint = (edgeSegmentLength - fromBorderDist) / edgeSegmentLength;
  503. controlnodeFromPos = {};
  504. controlnodeFromPos.x = (fromBorderPoint) * this.from.x + (1 - fromBorderPoint) * this.to.x;
  505. controlnodeFromPos.y = (fromBorderPoint) * this.from.y + (1 - fromBorderPoint) * this.to.y;
  506. }
  507. return controlnodeFromPos;
  508. }
  509. /**
  510. * this calculates the position of the control nodes on the edges of the parent nodes.
  511. *
  512. * @param ctx
  513. * @returns {{from: {x: number, y: number}, to: {x: *, y: *}}}
  514. */
  515. getControlNodeToPosition(ctx) {
  516. // draw arrow head
  517. var controlnodeToPos;
  518. if (this.options.smooth.enabled == true) {
  519. controlnodeToPos = this._findBorderPositionBezier(false, ctx);
  520. }
  521. else {
  522. var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  523. var dx = (this.to.x - this.from.x);
  524. var dy = (this.to.y - this.from.y);
  525. var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
  526. var toBorderDist = this.to.distanceToBorder(ctx, angle);
  527. var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength;
  528. controlnodeToPos = {};
  529. controlnodeToPos.x = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x;
  530. controlnodeToPos.y = (1 - toBorderPoint) * this.from.y + toBorderPoint * this.to.y;
  531. }
  532. return controlnodeToPos;
  533. }
  534. }
  535. export default Edge;