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.

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