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.

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