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.

432 lines
13 KiB

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