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.

542 lines
18 KiB

9 years ago
9 years ago
9 years ago
10 years ago
  1. var util = require('../../../util');
  2. import Label from './shared/Label'
  3. import CubicBezierEdge from './edges/CubicBezierEdge'
  4. import BezierEdgeDynamic from './edges/BezierEdgeDynamic'
  5. import BezierEdgeStatic from './edges/BezierEdgeStatic'
  6. import StraightEdge from './edges/StraightEdge'
  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.colorDirty = true;
  40. this.baseWidth = this.options.width;
  41. this.baseFontSize = this.options.font.size;
  42. this.from = undefined; // a node
  43. this.to = undefined; // a node
  44. this.edgeType = undefined;
  45. this.connected = false;
  46. this.labelModule = new Label(this.body, this.options, true /* It's an edge label */);
  47. this.setOptions(options);
  48. }
  49. /**
  50. * Set or overwrite options for the edge
  51. * @param {Object} options an object with options
  52. * @param doNotEmit
  53. */
  54. setOptions(options) {
  55. if (!options) {
  56. return;
  57. }
  58. this.colorDirty = true;
  59. Edge.parseOptions(this.options, options, true, this.globalOptions);
  60. if (options.id !== undefined) {this.id = options.id;}
  61. if (options.from !== undefined) {this.fromId = options.from;}
  62. if (options.to !== undefined) {this.toId = options.to;}
  63. if (options.title !== undefined) {this.title = options.title;}
  64. if (options.value !== undefined) {options.value = parseFloat(options.value);}
  65. // update label Module
  66. this.updateLabelModule(options);
  67. this.labelModule.propagateFonts(this.edgeOptions, options, this.defaultOptions);
  68. let dataChanged = this.updateEdgeType();
  69. // if anything has been updates, reset the selection width and the hover width
  70. this._setInteractionWidths();
  71. // A node is connected when it has a from and to node that both exist in the network.body.nodes.
  72. this.connect();
  73. if (options.hidden !== undefined || options.physics !== undefined) {
  74. dataChanged = true;
  75. }
  76. return dataChanged;
  77. }
  78. static parseOptions(parentOptions, newOptions, allowDeletion = false, globalOptions = {}) {
  79. var fields = [
  80. 'arrowStrikethrough',
  81. 'id',
  82. 'from',
  83. 'hidden',
  84. 'hoverWidth',
  85. 'label',
  86. 'labelHighlightBold',
  87. 'length',
  88. 'line',
  89. 'opacity',
  90. 'physics',
  91. 'scaling',
  92. 'selectionWidth',
  93. 'selfReferenceSize',
  94. 'to',
  95. 'title',
  96. 'value',
  97. 'width'
  98. ];
  99. // only deep extend the items in the field array. These do not have shorthand.
  100. util.selectiveDeepExtend(fields, parentOptions, newOptions, allowDeletion);
  101. util.mergeOptions(parentOptions, newOptions, 'smooth', allowDeletion, globalOptions);
  102. util.mergeOptions(parentOptions, newOptions, 'shadow', allowDeletion, globalOptions);
  103. if (newOptions.dashes !== undefined && newOptions.dashes !== null) {
  104. parentOptions.dashes = newOptions.dashes;
  105. }
  106. else if (allowDeletion === true && newOptions.dashes === null) {
  107. parentOptions.dashes = Object.create(globalOptions.dashes); // this sets the pointer of the option back to the global option.
  108. }
  109. // set the scaling newOptions
  110. if (newOptions.scaling !== undefined && newOptions.scaling !== null) {
  111. if (newOptions.scaling.min !== undefined) {parentOptions.scaling.min = newOptions.scaling.min;}
  112. if (newOptions.scaling.max !== undefined) {parentOptions.scaling.max = newOptions.scaling.max;}
  113. util.mergeOptions(parentOptions.scaling, newOptions.scaling, 'label', allowDeletion, globalOptions.scaling);
  114. }
  115. else if (allowDeletion === true && newOptions.scaling === null) {
  116. parentOptions.scaling = Object.create(globalOptions.scaling); // this sets the pointer of the option back to the global option.
  117. }
  118. // handle multiple input cases for arrows
  119. if (newOptions.arrows !== undefined && newOptions.arrows !== null) {
  120. if (typeof newOptions.arrows === 'string') {
  121. let arrows = newOptions.arrows.toLowerCase();
  122. parentOptions.arrows.to.enabled = arrows.indexOf("to") != -1;
  123. parentOptions.arrows.middle.enabled = arrows.indexOf("middle") != -1;
  124. parentOptions.arrows.from.enabled = arrows.indexOf("from") != -1;
  125. }
  126. else if (typeof newOptions.arrows === 'object') {
  127. util.mergeOptions(parentOptions.arrows, newOptions.arrows, 'to', allowDeletion, globalOptions.arrows);
  128. util.mergeOptions(parentOptions.arrows, newOptions.arrows, 'middle', allowDeletion, globalOptions.arrows);
  129. util.mergeOptions(parentOptions.arrows, newOptions.arrows, 'from', allowDeletion, globalOptions.arrows);
  130. }
  131. else {
  132. throw new Error("The arrow newOptions can only be an object or a string. Refer to the documentation. You used:" + JSON.stringify(newOptions.arrows));
  133. }
  134. }
  135. else if (allowDeletion === true && newOptions.arrows === null) {
  136. parentOptions.arrows = Object.create(globalOptions.arrows); // this sets the pointer of the option back to the global option.
  137. }
  138. // handle multiple input cases for color
  139. if (newOptions.color !== undefined && newOptions.color !== null) {
  140. // make a copy of the parent object in case this is referring to the global one (due to object create once, then update)
  141. parentOptions.color = util.deepExtend({}, parentOptions.color, true);
  142. if (util.isString(newOptions.color)) {
  143. parentOptions.color.color = newOptions.color;
  144. parentOptions.color.highlight = newOptions.color;
  145. parentOptions.color.hover = newOptions.color;
  146. parentOptions.color.inherit = false;
  147. }
  148. else {
  149. let colorsDefined = false;
  150. if (newOptions.color.color !== undefined) {parentOptions.color.color = newOptions.color.color; colorsDefined = true;}
  151. if (newOptions.color.highlight !== undefined) {parentOptions.color.highlight = newOptions.color.highlight; colorsDefined = true;}
  152. if (newOptions.color.hover !== undefined) {parentOptions.color.hover = newOptions.color.hover; colorsDefined = true;}
  153. if (newOptions.color.inherit !== undefined) {parentOptions.color.inherit = newOptions.color.inherit;}
  154. if (newOptions.color.opacity !== undefined) {parentOptions.color.opacity = Math.min(1,Math.max(0,newOptions.color.opacity));}
  155. if (newOptions.color.inherit === undefined && colorsDefined === true) {
  156. parentOptions.color.inherit = false;
  157. }
  158. }
  159. }
  160. else if (allowDeletion === true && newOptions.color === null) {
  161. parentOptions.color = util.bridgeObject(globalOptions.color); // set the object back to the global options
  162. }
  163. // handle the font settings
  164. if (newOptions.font !== undefined && newOptions.font !== null) {
  165. Label.parseOptions(parentOptions.font, newOptions);
  166. }
  167. else if (allowDeletion === true && newOptions.font === null) {
  168. parentOptions.font = util.bridgeObject(globalOptions.font); // set the object back to the global options
  169. }
  170. }
  171. /**
  172. * update the options in the label module
  173. */
  174. updateLabelModule(options) {
  175. this.labelModule.setOptions(this.options, true);
  176. if (this.labelModule.baseSize !== undefined) {
  177. this.baseFontSize = this.labelModule.baseSize;
  178. }
  179. this.labelModule.constrain(this.edgeOptions, options, this.defaultOptions);
  180. }
  181. /**
  182. * update the edge type, set the options
  183. * @returns {boolean}
  184. */
  185. updateEdgeType() {
  186. let dataChanged = false;
  187. let changeInType = true;
  188. let smooth = this.options.smooth;
  189. if (this.edgeType !== undefined) {
  190. if (this.edgeType instanceof BezierEdgeDynamic && smooth.enabled === true && smooth.type === 'dynamic') {changeInType = false;}
  191. if (this.edgeType instanceof CubicBezierEdge && smooth.enabled === true && smooth.type === 'cubicBezier') {changeInType = false;}
  192. if (this.edgeType instanceof BezierEdgeStatic && smooth.enabled === true && smooth.type !== 'dynamic' && smooth.type !== 'cubicBezier') {changeInType = false;}
  193. if (this.edgeType instanceof StraightEdge && smooth.enabled === false) {changeInType = false;}
  194. if (changeInType === true) {
  195. dataChanged = this.cleanup();
  196. }
  197. }
  198. if (changeInType === true) {
  199. if (this.options.smooth.enabled === true) {
  200. if (this.options.smooth.type === 'dynamic') {
  201. dataChanged = true;
  202. this.edgeType = new BezierEdgeDynamic(this.options, this.body, this.labelModule);
  203. }
  204. else if (this.options.smooth.type === 'cubicBezier') {
  205. this.edgeType = new CubicBezierEdge(this.options, this.body, this.labelModule);
  206. }
  207. else {
  208. this.edgeType = new BezierEdgeStatic(this.options, this.body, this.labelModule);
  209. }
  210. }
  211. else {
  212. this.edgeType = new StraightEdge(this.options, this.body, this.labelModule);
  213. }
  214. }
  215. else {
  216. // if nothing changes, we just set the options.
  217. this.edgeType.setOptions(this.options);
  218. }
  219. return dataChanged;
  220. }
  221. /**
  222. * Connect an edge to its nodes
  223. */
  224. connect() {
  225. this.disconnect();
  226. this.from = this.body.nodes[this.fromId] || undefined;
  227. this.to = this.body.nodes[this.toId] || undefined;
  228. this.connected = (this.from !== undefined && this.to !== undefined);
  229. if (this.connected === true) {
  230. this.from.attachEdge(this);
  231. this.to.attachEdge(this);
  232. }
  233. else {
  234. if (this.from) {
  235. this.from.detachEdge(this);
  236. }
  237. if (this.to) {
  238. this.to.detachEdge(this);
  239. }
  240. }
  241. this.edgeType.connect();
  242. }
  243. /**
  244. * Disconnect an edge from its nodes
  245. */
  246. disconnect() {
  247. if (this.from) {
  248. this.from.detachEdge(this);
  249. this.from = undefined;
  250. }
  251. if (this.to) {
  252. this.to.detachEdge(this);
  253. this.to = undefined;
  254. }
  255. this.connected = false;
  256. }
  257. /**
  258. * get the title of this edge.
  259. * @return {string} title The title of the edge, or undefined when no title
  260. * has been set.
  261. */
  262. getTitle() {
  263. return this.title;
  264. }
  265. /**
  266. * check if this node is selecte
  267. * @return {boolean} selected True if node is selected, else false
  268. */
  269. isSelected() {
  270. return this.selected;
  271. }
  272. /**
  273. * Retrieve the value of the edge. Can be undefined
  274. * @return {Number} value
  275. */
  276. getValue() {
  277. return this.options.value;
  278. }
  279. /**
  280. * Adjust the value range of the edge. The edge will adjust it's width
  281. * based on its value.
  282. * @param {Number} min
  283. * @param {Number} max
  284. * @param total
  285. */
  286. setValueRange(min, max, total) {
  287. if (this.options.value !== undefined) {
  288. var scale = this.options.scaling.customScalingFunction(min, max, total, this.options.value);
  289. var widthDiff = this.options.scaling.max - this.options.scaling.min;
  290. if (this.options.scaling.label.enabled === true) {
  291. var fontDiff = this.options.scaling.label.max - this.options.scaling.label.min;
  292. this.options.font.size = this.options.scaling.label.min + scale * fontDiff;
  293. }
  294. this.options.width = this.options.scaling.min + scale * widthDiff;
  295. }
  296. else {
  297. this.options.width = this.baseWidth;
  298. this.options.font.size = this.baseFontSize;
  299. }
  300. this._setInteractionWidths();
  301. this.updateLabelModule();
  302. }
  303. _setInteractionWidths() {
  304. if (typeof this.options.hoverWidth === 'function') {
  305. this.edgeType.hoverWidth = this.options.hoverWidth(this.options.width);
  306. }
  307. else {
  308. this.edgeType.hoverWidth = this.options.hoverWidth + this.options.width;
  309. }
  310. if (typeof this.options.selectionWidth === 'function') {
  311. this.edgeType.selectionWidth = this.options.selectionWidth(this.options.width);
  312. }
  313. else {
  314. this.edgeType.selectionWidth = this.options.selectionWidth + this.options.width;
  315. }
  316. }
  317. /**
  318. * Redraw a edge
  319. * Draw this edge in the given canvas
  320. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  321. * @param {CanvasRenderingContext2D} ctx
  322. */
  323. draw(ctx) {
  324. // get the via node from the edge type
  325. let viaNode = this.edgeType.getViaNode();
  326. let arrowData = {};
  327. // restore edge targets to defaults
  328. this.edgeType.fromPoint = this.edgeType.from;
  329. this.edgeType.toPoint = this.edgeType.to;
  330. // from and to arrows give a different end point for edges. we set them here
  331. if (this.options.arrows.from.enabled === true) {
  332. arrowData.from = this.edgeType.getArrowData(ctx,'from', viaNode, this.selected, this.hover);
  333. if (this.options.arrowStrikethrough === false)
  334. this.edgeType.fromPoint = arrowData.from.core;
  335. }
  336. if (this.options.arrows.to.enabled === true) {
  337. arrowData.to = this.edgeType.getArrowData(ctx,'to', viaNode, this.selected, this.hover);
  338. if (this.options.arrowStrikethrough === false)
  339. this.edgeType.toPoint = arrowData.to.core;
  340. }
  341. // the middle arrow depends on the line, which can depend on the to and from arrows so we do this one lastly.
  342. if (this.options.arrows.middle.enabled === true) {
  343. arrowData.middle = this.edgeType.getArrowData(ctx,'middle', viaNode, this.selected, this.hover);
  344. }
  345. // draw everything
  346. this.edgeType.drawLine(ctx, this.selected, this.hover, viaNode);
  347. this.drawArrows(ctx, arrowData);
  348. this.drawLabel (ctx, viaNode);
  349. }
  350. drawArrows(ctx, arrowData) {
  351. if (this.options.arrows.from.enabled === true) {this.edgeType.drawArrowHead(ctx, this.selected, this.hover, arrowData.from);}
  352. if (this.options.arrows.middle.enabled === true) {this.edgeType.drawArrowHead(ctx, this.selected, this.hover, arrowData.middle);}
  353. if (this.options.arrows.to.enabled === true) {this.edgeType.drawArrowHead(ctx, this.selected, this.hover, arrowData.to);}
  354. }
  355. drawLabel(ctx, viaNode) {
  356. if (this.options.label !== undefined) {
  357. // set style
  358. var node1 = this.from;
  359. var node2 = this.to;
  360. var selected = (this.from.selected || this.to.selected || this.selected);
  361. if (node1.id != node2.id) {
  362. this.labelModule.pointToSelf = false;
  363. var point = this.edgeType.getPoint(0.5, viaNode);
  364. ctx.save();
  365. // if the label has to be rotated:
  366. if (this.options.font.align !== "horizontal") {
  367. this.labelModule.calculateLabelSize(ctx,selected,point.x,point.y);
  368. ctx.translate(point.x, this.labelModule.size.yLine);
  369. this._rotateForLabelAlignment(ctx);
  370. }
  371. // draw the label
  372. this.labelModule.draw(ctx, point.x, point.y, selected);
  373. ctx.restore();
  374. }
  375. else {
  376. // Ignore the orientations.
  377. this.labelModule.pointToSelf = true;
  378. var x, y;
  379. var radius = this.options.selfReferenceSize;
  380. if (node1.shape.width > node1.shape.height) {
  381. x = node1.x + node1.shape.width * 0.5;
  382. y = node1.y - radius;
  383. }
  384. else {
  385. x = node1.x + radius;
  386. y = node1.y - node1.shape.height * 0.5;
  387. }
  388. point = this._pointOnCircle(x, y, radius, 0.125);
  389. this.labelModule.draw(ctx, point.x, point.y, selected);
  390. }
  391. }
  392. }
  393. /**
  394. * Check if this object is overlapping with the provided object
  395. * @param {Object} obj an object with parameters left, top
  396. * @return {boolean} True if location is located on the edge
  397. */
  398. isOverlappingWith(obj) {
  399. if (this.connected) {
  400. var distMax = 10;
  401. var xFrom = this.from.x;
  402. var yFrom = this.from.y;
  403. var xTo = this.to.x;
  404. var yTo = this.to.y;
  405. var xObj = obj.left;
  406. var yObj = obj.top;
  407. var dist = this.edgeType.getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj);
  408. return (dist < distMax);
  409. }
  410. else {
  411. return false
  412. }
  413. }
  414. /**
  415. * Rotates the canvas so the text is most readable
  416. * @param {CanvasRenderingContext2D} ctx
  417. * @private
  418. */
  419. _rotateForLabelAlignment(ctx) {
  420. var dy = this.from.y - this.to.y;
  421. var dx = this.from.x - this.to.x;
  422. var angleInDegrees = Math.atan2(dy, dx);
  423. // rotate so label it is readable
  424. if ((angleInDegrees < -1 && dx < 0) || (angleInDegrees > 0 && dx < 0)) {
  425. angleInDegrees = angleInDegrees + Math.PI;
  426. }
  427. ctx.rotate(angleInDegrees);
  428. }
  429. /**
  430. * Get a point on a circle
  431. * @param {Number} x
  432. * @param {Number} y
  433. * @param {Number} radius
  434. * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
  435. * @return {Object} point
  436. * @private
  437. */
  438. _pointOnCircle(x, y, radius, percentage) {
  439. var angle = percentage * 2 * Math.PI;
  440. return {
  441. x: x + radius * Math.cos(angle),
  442. y: y - radius * Math.sin(angle)
  443. }
  444. }
  445. select() {
  446. this.selected = true;
  447. }
  448. unselect() {
  449. this.selected = false;
  450. }
  451. /**
  452. * cleans all required things on delete
  453. * @returns {*}
  454. */
  455. cleanup() {
  456. return this.edgeType.cleanup();
  457. }
  458. }
  459. export default Edge;