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.

498 lines
15 KiB

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