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.

425 lines
12 KiB

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