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.

649 lines
22 KiB

9 years ago
9 years ago
9 years ago
10 years ago
  1. var util = require('../../../util');
  2. var Label = require('./shared/Label').default;
  3. var CubicBezierEdge = require('./edges/CubicBezierEdge').default;
  4. var BezierEdgeDynamic = require('./edges/BezierEdgeDynamic').default;
  5. var BezierEdgeStatic = require('./edges/BezierEdgeStatic').default;
  6. var StraightEdge = require('./edges/StraightEdge').default;
  7. /**
  8. * @class Edge
  9. *
  10. * A edge connects two nodes
  11. * @param {Object} properties Object with options. Must contain
  12. * At least options from and to.
  13. * Available options: from (number),
  14. * to (number), label (string, color (string),
  15. * width (number), style (string),
  16. * length (number), title (string)
  17. * @param {Network} network A Network object, used to find and edge to
  18. * nodes.
  19. * @param {Object} constants An object with default values for
  20. * example for the color
  21. */
  22. class Edge {
  23. constructor(options, body, globalOptions, defaultOptions, edgeOptions) {
  24. if (body === undefined) {
  25. throw "No body provided";
  26. }
  27. this.options = util.bridgeObject(globalOptions);
  28. this.globalOptions = globalOptions;
  29. this.defaultOptions = defaultOptions;
  30. this.edgeOptions = edgeOptions;
  31. this.body = body;
  32. // initialize variables
  33. this.id = undefined;
  34. this.fromId = undefined;
  35. this.toId = undefined;
  36. this.selected = false;
  37. this.hover = false;
  38. this.labelDirty = true;
  39. this.baseWidth = this.options.width;
  40. this.baseFontSize = this.options.font.size;
  41. this.from = undefined; // a node
  42. this.to = undefined; // a node
  43. this.edgeType = undefined;
  44. this.connected = false;
  45. this.labelModule = new Label(this.body, this.options, true /* It's an edge label */);
  46. this.setOptions(options);
  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. Edge.parseOptions(this.options, options, true, this.globalOptions);
  58. if (options.id !== undefined) {
  59. this.id = options.id;
  60. }
  61. if (options.from !== undefined) {
  62. this.fromId = options.from;
  63. }
  64. if (options.to !== undefined) {
  65. this.toId = options.to;
  66. }
  67. if (options.title !== undefined) {
  68. this.title = options.title;
  69. }
  70. if (options.value !== undefined) {
  71. options.value = parseFloat(options.value);
  72. }
  73. this.choosify(options);
  74. // update label Module
  75. this.updateLabelModule(options);
  76. this.labelModule.propagateFonts(this.edgeOptions, options, this.defaultOptions);
  77. let dataChanged = this.updateEdgeType();
  78. // if anything has been updates, reset the selection width and the hover width
  79. this._setInteractionWidths();
  80. // A node is connected when it has a from and to node that both exist in the network.body.nodes.
  81. this.connect();
  82. if (options.hidden !== undefined || options.physics !== undefined) {
  83. dataChanged = true;
  84. }
  85. return dataChanged;
  86. }
  87. static parseOptions(parentOptions, newOptions, allowDeletion = false, globalOptions = {}) {
  88. var fields = [
  89. 'arrowStrikethrough',
  90. 'id',
  91. 'from',
  92. 'hidden',
  93. 'hoverWidth',
  94. 'label',
  95. 'labelHighlightBold',
  96. 'length',
  97. 'line',
  98. 'opacity',
  99. 'physics',
  100. 'scaling',
  101. 'selectionWidth',
  102. 'selfReferenceSize',
  103. 'to',
  104. 'title',
  105. 'value',
  106. 'width'
  107. ];
  108. // only deep extend the items in the field array. These do not have shorthand.
  109. util.selectiveDeepExtend(fields, parentOptions, newOptions, allowDeletion);
  110. util.mergeOptions(parentOptions, newOptions, 'smooth', allowDeletion, globalOptions);
  111. util.mergeOptions(parentOptions, newOptions, 'shadow', allowDeletion, globalOptions);
  112. if (newOptions.dashes !== undefined && newOptions.dashes !== null) {
  113. parentOptions.dashes = newOptions.dashes;
  114. }
  115. else if (allowDeletion === true && newOptions.dashes === null) {
  116. parentOptions.dashes = Object.create(globalOptions.dashes); // this sets the pointer of the option back to the global option.
  117. }
  118. // set the scaling newOptions
  119. if (newOptions.scaling !== undefined && newOptions.scaling !== null) {
  120. if (newOptions.scaling.min !== undefined) {parentOptions.scaling.min = newOptions.scaling.min;}
  121. if (newOptions.scaling.max !== undefined) {parentOptions.scaling.max = newOptions.scaling.max;}
  122. util.mergeOptions(parentOptions.scaling, newOptions.scaling, 'label', allowDeletion, globalOptions.scaling);
  123. }
  124. else if (allowDeletion === true && newOptions.scaling === null) {
  125. parentOptions.scaling = Object.create(globalOptions.scaling); // this sets the pointer of the option back to the global option.
  126. }
  127. // handle multiple input cases for arrows
  128. if (newOptions.arrows !== undefined && newOptions.arrows !== null) {
  129. if (typeof newOptions.arrows === 'string') {
  130. let arrows = newOptions.arrows.toLowerCase();
  131. parentOptions.arrows.to.enabled = arrows.indexOf("to") != -1;
  132. parentOptions.arrows.middle.enabled = arrows.indexOf("middle") != -1;
  133. parentOptions.arrows.from.enabled = arrows.indexOf("from") != -1;
  134. }
  135. else if (typeof newOptions.arrows === 'object') {
  136. util.mergeOptions(parentOptions.arrows, newOptions.arrows, 'to', allowDeletion, globalOptions.arrows);
  137. util.mergeOptions(parentOptions.arrows, newOptions.arrows, 'middle', allowDeletion, globalOptions.arrows);
  138. util.mergeOptions(parentOptions.arrows, newOptions.arrows, 'from', allowDeletion, globalOptions.arrows);
  139. }
  140. else {
  141. throw new Error("The arrow newOptions can only be an object or a string. Refer to the documentation. You used:" + JSON.stringify(newOptions.arrows));
  142. }
  143. }
  144. else if (allowDeletion === true && newOptions.arrows === null) {
  145. parentOptions.arrows = Object.create(globalOptions.arrows); // this sets the pointer of the option back to the global option.
  146. }
  147. // handle multiple input cases for color
  148. if (newOptions.color !== undefined && newOptions.color !== null) {
  149. // make a copy of the parent object in case this is referring to the global one (due to object create once, then update)
  150. parentOptions.color = util.deepExtend({}, parentOptions.color, true);
  151. if (util.isString(newOptions.color)) {
  152. parentOptions.color.color = newOptions.color;
  153. parentOptions.color.highlight = newOptions.color;
  154. parentOptions.color.hover = newOptions.color;
  155. parentOptions.color.inherit = false;
  156. }
  157. else {
  158. let colorsDefined = false;
  159. if (newOptions.color.color !== undefined) {parentOptions.color.color = newOptions.color.color; colorsDefined = true;}
  160. if (newOptions.color.highlight !== undefined) {parentOptions.color.highlight = newOptions.color.highlight; colorsDefined = true;}
  161. if (newOptions.color.hover !== undefined) {parentOptions.color.hover = newOptions.color.hover; colorsDefined = true;}
  162. if (newOptions.color.inherit !== undefined) {parentOptions.color.inherit = newOptions.color.inherit;}
  163. if (newOptions.color.opacity !== undefined) {parentOptions.color.opacity = Math.min(1,Math.max(0,newOptions.color.opacity));}
  164. if (newOptions.color.inherit === undefined && colorsDefined === true) {
  165. parentOptions.color.inherit = false;
  166. }
  167. }
  168. }
  169. else if (allowDeletion === true && newOptions.color === null) {
  170. parentOptions.color = util.bridgeObject(globalOptions.color); // set the object back to the global options
  171. }
  172. // handle the font settings
  173. if (newOptions.font !== undefined && newOptions.font !== null) {
  174. Label.parseOptions(parentOptions.font, newOptions);
  175. }
  176. else if (allowDeletion === true && newOptions.font === null) {
  177. parentOptions.font = util.bridgeObject(globalOptions.font); // set the object back to the global options
  178. }
  179. }
  180. choosify(options) {
  181. this.chooser = true;
  182. let pile = [options, this.options, this.edgeOptions, this.defaultOptions];
  183. let chosen = util.topMost(pile, 'chosen');
  184. if (typeof chosen === 'boolean') {
  185. this.chooser = chosen;
  186. } else if (typeof chosen === 'object') {
  187. let chosenEdge = util.topMost(pile, ['chosen', 'edge']);
  188. if ((typeof chosenEdge === 'boolean') || (typeof chosenEdge === 'function')) {
  189. this.chooser = chosenEdge;
  190. }
  191. }
  192. }
  193. getFormattingValues() {
  194. let toArrow = (this.options.arrows.to === true) || (this.options.arrows.to.enabled === true)
  195. let fromArrow = (this.options.arrows.from === true) || (this.options.arrows.from.enabled === true)
  196. let middleArrow = (this.options.arrows.middle === true) || (this.options.arrows.middle.enabled === true)
  197. let inheritsColor = this.options.color.inherit;
  198. let values = {
  199. toArrow: toArrow,
  200. toArrowScale: this.options.arrows.to.scaleFactor,
  201. toArrowType: this.options.arrows.to.type,
  202. middleArrow: middleArrow,
  203. middleArrowScale: this.options.arrows.middle.scaleFactor,
  204. middleArrowType: this.options.arrows.middle.type,
  205. fromArrow: fromArrow,
  206. fromArrowScale: this.options.arrows.from.scaleFactor,
  207. fromArrowType: this.options.arrows.from.type,
  208. arrowStrikethrough: this.options.arrowStrikethrough,
  209. color: (inheritsColor? undefined : this.options.color.color),
  210. inheritsColor: inheritsColor,
  211. opacity: this.options.color.opacity,
  212. hidden: this.options.hidden,
  213. length: this.options.length,
  214. shadow: this.options.shadow.enabled,
  215. shadowColor: this.options.shadow.color,
  216. shadowSize: this.options.shadow.size,
  217. shadowX: this.options.shadow.x,
  218. shadowY: this.options.shadow.y,
  219. dashes: this.options.dashes,
  220. width: this.options.width
  221. };
  222. if (this.selected || this.hover) {
  223. if (this.chooser === true) {
  224. if (this.selected) {
  225. let selectedWidth = this.options.selectionWidth;
  226. if (typeof selectedWidth === 'function') {
  227. values.width = selectedWidth(values.width);
  228. } else if (typeof selectedWidth === 'number') {
  229. values.width += selectedWidth;
  230. }
  231. values.width = Math.max(values.width, 0.3 / this.body.view.scale);
  232. values.color = this.options.color.highlight;
  233. values.shadow = this.options.shadow.enabled;
  234. } else if (this.hover) {
  235. let hoverWidth = this.options.hoverWidth;
  236. if (typeof hoverWidth === 'function') {
  237. values.width = hoverWidth(values.width);
  238. } else if (typeof hoverWidth === 'number') {
  239. values.width += hoverWidth;
  240. }
  241. values.width = Math.max(values.width, 0.3 / this.body.view.scale);
  242. values.color = this.options.color.hover;
  243. values.shadow = this.options.shadow.enabled;
  244. }
  245. } else if (typeof this.chooser === 'function') {
  246. this.chooser(values, this.options.id, this.selected, this.hover);
  247. if (values.color !== undefined) {
  248. values.inheritsColor = false;
  249. }
  250. if (values.shadow === false) {
  251. if ((values.shadowColor !== this.options.shadow.color) ||
  252. (values.shadowSize !== this.options.shadow.size) ||
  253. (values.shadowX !== this.options.shadow.x) ||
  254. (values.shadowY !== this.options.shadow.y)) {
  255. values.shadow = true;
  256. }
  257. }
  258. }
  259. } else {
  260. values.shadow = this.options.shadow.enabled;
  261. values.width = Math.max(values.width, 0.3 / this.body.view.scale);
  262. }
  263. return values;
  264. }
  265. /**
  266. * update the options in the label module
  267. */
  268. updateLabelModule(options) {
  269. this.labelModule.setOptions(this.options, true);
  270. if (this.labelModule.baseSize !== undefined) {
  271. this.baseFontSize = this.labelModule.baseSize;
  272. }
  273. this.labelModule.constrain(this.edgeOptions, options, this.defaultOptions);
  274. this.labelModule.choosify(this.edgeOptions, options, this.defaultOptions);
  275. }
  276. /**
  277. * update the edge type, set the options
  278. * @returns {boolean}
  279. */
  280. updateEdgeType() {
  281. let smooth = this.options.smooth;
  282. let dataChanged = false;
  283. let changeInType = true;
  284. if (this.edgeType !== undefined) {
  285. if ((((this.edgeType instanceof BezierEdgeDynamic) &&
  286. (smooth.enabled === true) &&
  287. (smooth.type === 'dynamic'))) ||
  288. (((this.edgeType instanceof CubicBezierEdge) &&
  289. (smooth.enabled === true) &&
  290. (smooth.type === 'cubicBezier'))) ||
  291. (((this.edgeType instanceof BezierEdgeStatic) &&
  292. (smooth.enabled === true) &&
  293. (smooth.type !== 'dynamic') &&
  294. (smooth.type !== 'cubicBezier'))) ||
  295. (((this.edgeType instanceof StraightEdge) &&
  296. (smooth.type.enabled === false)))) {
  297. changeInType = false;
  298. }
  299. if (changeInType === true) {
  300. dataChanged = this.cleanup();
  301. }
  302. }
  303. if (changeInType === true) {
  304. if (smooth.enabled === true) {
  305. if (smooth.type === 'dynamic') {
  306. dataChanged = true;
  307. this.edgeType = new BezierEdgeDynamic(this.options, this.body, this.labelModule);
  308. } else if (smooth.type === 'cubicBezier') {
  309. this.edgeType = new CubicBezierEdge(this.options, this.body, this.labelModule);
  310. } else {
  311. this.edgeType = new BezierEdgeStatic(this.options, this.body, this.labelModule);
  312. }
  313. } else {
  314. this.edgeType = new StraightEdge(this.options, this.body, this.labelModule);
  315. }
  316. } else { // if nothing changes, we just set the options.
  317. this.edgeType.setOptions(this.options);
  318. }
  319. return dataChanged;
  320. }
  321. /**
  322. * Connect an edge to its nodes
  323. */
  324. connect() {
  325. this.disconnect();
  326. this.from = this.body.nodes[this.fromId] || undefined;
  327. this.to = this.body.nodes[this.toId] || undefined;
  328. this.connected = (this.from !== undefined && this.to !== undefined);
  329. if (this.connected === true) {
  330. this.from.attachEdge(this);
  331. this.to.attachEdge(this);
  332. }
  333. else {
  334. if (this.from) {
  335. this.from.detachEdge(this);
  336. }
  337. if (this.to) {
  338. this.to.detachEdge(this);
  339. }
  340. }
  341. this.edgeType.connect();
  342. }
  343. /**
  344. * Disconnect an edge from its nodes
  345. */
  346. disconnect() {
  347. if (this.from) {
  348. this.from.detachEdge(this);
  349. this.from = undefined;
  350. }
  351. if (this.to) {
  352. this.to.detachEdge(this);
  353. this.to = undefined;
  354. }
  355. this.connected = false;
  356. }
  357. /**
  358. * get the title of this edge.
  359. * @return {string} title The title of the edge, or undefined when no title
  360. * has been set.
  361. */
  362. getTitle() {
  363. return this.title;
  364. }
  365. /**
  366. * check if this node is selecte
  367. * @return {boolean} selected True if node is selected, else false
  368. */
  369. isSelected() {
  370. return this.selected;
  371. }
  372. /**
  373. * Retrieve the value of the edge. Can be undefined
  374. * @return {Number} value
  375. */
  376. getValue() {
  377. return this.options.value;
  378. }
  379. /**
  380. * Adjust the value range of the edge. The edge will adjust it's width
  381. * based on its value.
  382. * @param {Number} min
  383. * @param {Number} max
  384. * @param total
  385. */
  386. setValueRange(min, max, total) {
  387. if (this.options.value !== undefined) {
  388. var scale = this.options.scaling.customScalingFunction(min, max, total, this.options.value);
  389. var widthDiff = this.options.scaling.max - this.options.scaling.min;
  390. if (this.options.scaling.label.enabled === true) {
  391. var fontDiff = this.options.scaling.label.max - this.options.scaling.label.min;
  392. this.options.font.size = this.options.scaling.label.min + scale * fontDiff;
  393. }
  394. this.options.width = this.options.scaling.min + scale * widthDiff;
  395. }
  396. else {
  397. this.options.width = this.baseWidth;
  398. this.options.font.size = this.baseFontSize;
  399. }
  400. this._setInteractionWidths();
  401. this.updateLabelModule();
  402. }
  403. _setInteractionWidths() {
  404. if (typeof this.options.hoverWidth === 'function') {
  405. this.edgeType.hoverWidth = this.options.hoverWidth(this.options.width);
  406. } else {
  407. this.edgeType.hoverWidth = this.options.hoverWidth + this.options.width;
  408. }
  409. if (typeof this.options.selectionWidth === 'function') {
  410. this.edgeType.selectionWidth = this.options.selectionWidth(this.options.width);
  411. } else {
  412. this.edgeType.selectionWidth = this.options.selectionWidth + this.options.width;
  413. }
  414. }
  415. /**
  416. * Redraw a edge
  417. * Draw this edge in the given canvas
  418. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  419. * @param {CanvasRenderingContext2D} ctx
  420. */
  421. draw(ctx) {
  422. let values = this.getFormattingValues();
  423. if (values.hidden) {
  424. return;
  425. }
  426. // get the via node from the edge type
  427. let viaNode = this.edgeType.getViaNode();
  428. let arrowData = {};
  429. // restore edge targets to defaults
  430. this.edgeType.fromPoint = this.edgeType.from;
  431. this.edgeType.toPoint = this.edgeType.to;
  432. // from and to arrows give a different end point for edges. we set them here
  433. if (values.fromArrow) {
  434. arrowData.from = this.edgeType.getArrowData(ctx, 'from', viaNode, this.selected, this.hover, values);
  435. if (values.arrowStrikethrough === false)
  436. this.edgeType.fromPoint = arrowData.from.core;
  437. }
  438. if (values.toArrow) {
  439. arrowData.to = this.edgeType.getArrowData(ctx, 'to', viaNode, this.selected, this.hover, values);
  440. if (values.arrowStrikethrough === false)
  441. this.edgeType.toPoint = arrowData.to.core;
  442. }
  443. // the middle arrow depends on the line, which can depend on the to and from arrows so we do this one lastly.
  444. if (values.middleArrow) {
  445. arrowData.middle = this.edgeType.getArrowData(ctx,'middle', viaNode, this.selected, this.hover, values);
  446. }
  447. // draw everything
  448. this.edgeType.drawLine(ctx, values, this.selected, this.hover, viaNode);
  449. this.drawArrows(ctx, arrowData, values);
  450. this.drawLabel (ctx, viaNode);
  451. }
  452. drawArrows(ctx, arrowData, values) {
  453. if (values.fromArrow) {
  454. this.edgeType.drawArrowHead(ctx, values, this.selected, this.hover, arrowData.from);
  455. }
  456. if (values.middleArrow) {
  457. this.edgeType.drawArrowHead(ctx, values, this.selected, this.hover, arrowData.middle);
  458. }
  459. if (values.toArrow) {
  460. this.edgeType.drawArrowHead(ctx, values, this.selected, this.hover, arrowData.to);
  461. }
  462. }
  463. drawLabel(ctx, viaNode) {
  464. if (this.options.label !== undefined) {
  465. // set style
  466. var node1 = this.from;
  467. var node2 = this.to;
  468. var selected = (this.from.selected || this.to.selected || this.selected);
  469. if (node1.id != node2.id) {
  470. this.labelModule.pointToSelf = false;
  471. var point = this.edgeType.getPoint(0.5, viaNode);
  472. ctx.save();
  473. // if the label has to be rotated:
  474. if (this.options.font.align !== "horizontal") {
  475. this.labelModule.calculateLabelSize(ctx, selected, this.hover, point.x, point.y);
  476. ctx.translate(point.x, this.labelModule.size.yLine);
  477. this._rotateForLabelAlignment(ctx);
  478. }
  479. // draw the label
  480. this.labelModule.draw(ctx, point.x, point.y, selected, this.hover);
  481. ctx.restore();
  482. }
  483. else {
  484. // Ignore the orientations.
  485. this.labelModule.pointToSelf = true;
  486. var x, y;
  487. var radius = this.options.selfReferenceSize;
  488. if (node1.shape.width > node1.shape.height) {
  489. x = node1.x + node1.shape.width * 0.5;
  490. y = node1.y - radius;
  491. }
  492. else {
  493. x = node1.x + radius;
  494. y = node1.y - node1.shape.height * 0.5;
  495. }
  496. point = this._pointOnCircle(x, y, radius, 0.125);
  497. this.labelModule.draw(ctx, point.x, point.y, selected, this.hover);
  498. }
  499. }
  500. }
  501. /**
  502. * Check if this object is overlapping with the provided object
  503. * @param {Object} obj an object with parameters left, top
  504. * @return {boolean} True if location is located on the edge
  505. */
  506. isOverlappingWith(obj) {
  507. if (this.connected) {
  508. var distMax = 10;
  509. var xFrom = this.from.x;
  510. var yFrom = this.from.y;
  511. var xTo = this.to.x;
  512. var yTo = this.to.y;
  513. var xObj = obj.left;
  514. var yObj = obj.top;
  515. var dist = this.edgeType.getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj);
  516. return (dist < distMax);
  517. }
  518. else {
  519. return false
  520. }
  521. }
  522. /**
  523. * Rotates the canvas so the text is most readable
  524. * @param {CanvasRenderingContext2D} ctx
  525. * @private
  526. */
  527. _rotateForLabelAlignment(ctx) {
  528. var dy = this.from.y - this.to.y;
  529. var dx = this.from.x - this.to.x;
  530. var angleInDegrees = Math.atan2(dy, dx);
  531. // rotate so label it is readable
  532. if ((angleInDegrees < -1 && dx < 0) || (angleInDegrees > 0 && dx < 0)) {
  533. angleInDegrees = angleInDegrees + Math.PI;
  534. }
  535. ctx.rotate(angleInDegrees);
  536. }
  537. /**
  538. * Get a point on a circle
  539. * @param {Number} x
  540. * @param {Number} y
  541. * @param {Number} radius
  542. * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
  543. * @return {Object} point
  544. * @private
  545. */
  546. _pointOnCircle(x, y, radius, percentage) {
  547. var angle = percentage * 2 * Math.PI;
  548. return {
  549. x: x + radius * Math.cos(angle),
  550. y: y - radius * Math.sin(angle)
  551. }
  552. }
  553. select() {
  554. this.selected = true;
  555. }
  556. unselect() {
  557. this.selected = false;
  558. }
  559. /**
  560. * cleans all required things on delete
  561. * @returns {*}
  562. */
  563. cleanup() {
  564. return this.edgeType.cleanup();
  565. }
  566. }
  567. export default Edge;