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.

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