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.

781 lines
26 KiB

9 years ago
Network: Fix handling of multi-fonts (#3486) * The next fix on Travis unit test failure This is the next escalation on the war against the Travis unit tests failing (which came into being by yours truly). By accident, I could recreate the unit test failure on my development machine. This led to a more directed effort to squash the bug. The insight here is that test `(window === undefined)` fails, but `(typeof window === 'undefined`)` succeeds. This undoubtedly has to do with the special status `window` has as a global object. Changes: - Added check on presence of `window` in `Canvas._requestNextFrame()`, fixed local source errors. - Added catch clause in `CanvasRendered._determinePixelRatio()` - small fix: raised timeout for the network `worldCup2014` unit test * Preliminary refactoring in utils.js * Added unit tests for extend routines, commenting and small fixes * More unit tests for extend routines * - Completed unit tests for extend routines in - Small fixes and cleanup in `util.js` - Removed `util.protoExtend()`, not used anywhere * Added unit tests for known font options * Interim save before trying out another proto chain strategy * Fixed problem in first example #3408 * Removed silly file that shouldn't be there * Added unit test for multi-fonts * Comment edits * Verufy unit tests, small adjustments for groups * Further work on getting unit tests to work. PARTS NEED TO BE CLEANED UP! * Further tweaks to get unit tests working * All unit tests passing * Fixes due to linting * Small edits * Removed prototype handling from font pile * Fixes during testing examples of #3408 * Added unit test for edge labels, small fixes * Added unit tests for shorthand string fonts; some tests still failing * All unit tests pass * Removed Label.parseOptions() * Completed shorthand font tests, code cleanup, fixed choosify for edges * Addressed review comments * Addressed review comments, cleanup
7 years ago
Network: Fix handling of multi-fonts (#3486) * The next fix on Travis unit test failure This is the next escalation on the war against the Travis unit tests failing (which came into being by yours truly). By accident, I could recreate the unit test failure on my development machine. This led to a more directed effort to squash the bug. The insight here is that test `(window === undefined)` fails, but `(typeof window === 'undefined`)` succeeds. This undoubtedly has to do with the special status `window` has as a global object. Changes: - Added check on presence of `window` in `Canvas._requestNextFrame()`, fixed local source errors. - Added catch clause in `CanvasRendered._determinePixelRatio()` - small fix: raised timeout for the network `worldCup2014` unit test * Preliminary refactoring in utils.js * Added unit tests for extend routines, commenting and small fixes * More unit tests for extend routines * - Completed unit tests for extend routines in - Small fixes and cleanup in `util.js` - Removed `util.protoExtend()`, not used anywhere * Added unit tests for known font options * Interim save before trying out another proto chain strategy * Fixed problem in first example #3408 * Removed silly file that shouldn't be there * Added unit test for multi-fonts * Comment edits * Verufy unit tests, small adjustments for groups * Further work on getting unit tests to work. PARTS NEED TO BE CLEANED UP! * Further tweaks to get unit tests working * All unit tests passing * Fixes due to linting * Small edits * Removed prototype handling from font pile * Fixes during testing examples of #3408 * Added unit test for edge labels, small fixes * Added unit tests for shorthand string fonts; some tests still failing * All unit tests pass * Removed Label.parseOptions() * Completed shorthand font tests, code cleanup, fixed choosify for edges * Addressed review comments * Addressed review comments, cleanup
7 years ago
Network: Fix handling of multi-fonts (#3486) * The next fix on Travis unit test failure This is the next escalation on the war against the Travis unit tests failing (which came into being by yours truly). By accident, I could recreate the unit test failure on my development machine. This led to a more directed effort to squash the bug. The insight here is that test `(window === undefined)` fails, but `(typeof window === 'undefined`)` succeeds. This undoubtedly has to do with the special status `window` has as a global object. Changes: - Added check on presence of `window` in `Canvas._requestNextFrame()`, fixed local source errors. - Added catch clause in `CanvasRendered._determinePixelRatio()` - small fix: raised timeout for the network `worldCup2014` unit test * Preliminary refactoring in utils.js * Added unit tests for extend routines, commenting and small fixes * More unit tests for extend routines * - Completed unit tests for extend routines in - Small fixes and cleanup in `util.js` - Removed `util.protoExtend()`, not used anywhere * Added unit tests for known font options * Interim save before trying out another proto chain strategy * Fixed problem in first example #3408 * Removed silly file that shouldn't be there * Added unit test for multi-fonts * Comment edits * Verufy unit tests, small adjustments for groups * Further work on getting unit tests to work. PARTS NEED TO BE CLEANED UP! * Further tweaks to get unit tests working * All unit tests passing * Fixes due to linting * Small edits * Removed prototype handling from font pile * Fixes during testing examples of #3408 * Added unit test for edge labels, small fixes * Added unit tests for shorthand string fonts; some tests still failing * All unit tests pass * Removed Label.parseOptions() * Completed shorthand font tests, code cleanup, fixed choosify for edges * Addressed review comments * Addressed review comments, cleanup
7 years ago
9 years ago
9 years ago
Network: Fix handling of multi-fonts (#3486) * The next fix on Travis unit test failure This is the next escalation on the war against the Travis unit tests failing (which came into being by yours truly). By accident, I could recreate the unit test failure on my development machine. This led to a more directed effort to squash the bug. The insight here is that test `(window === undefined)` fails, but `(typeof window === 'undefined`)` succeeds. This undoubtedly has to do with the special status `window` has as a global object. Changes: - Added check on presence of `window` in `Canvas._requestNextFrame()`, fixed local source errors. - Added catch clause in `CanvasRendered._determinePixelRatio()` - small fix: raised timeout for the network `worldCup2014` unit test * Preliminary refactoring in utils.js * Added unit tests for extend routines, commenting and small fixes * More unit tests for extend routines * - Completed unit tests for extend routines in - Small fixes and cleanup in `util.js` - Removed `util.protoExtend()`, not used anywhere * Added unit tests for known font options * Interim save before trying out another proto chain strategy * Fixed problem in first example #3408 * Removed silly file that shouldn't be there * Added unit test for multi-fonts * Comment edits * Verufy unit tests, small adjustments for groups * Further work on getting unit tests to work. PARTS NEED TO BE CLEANED UP! * Further tweaks to get unit tests working * All unit tests passing * Fixes due to linting * Small edits * Removed prototype handling from font pile * Fixes during testing examples of #3408 * Added unit test for edge labels, small fixes * Added unit tests for shorthand string fonts; some tests still failing * All unit tests pass * Removed Label.parseOptions() * Completed shorthand font tests, code cleanup, fixed choosify for edges * Addressed review comments * Addressed review comments, cleanup
7 years ago
Network: Fix handling of multi-fonts (#3486) * The next fix on Travis unit test failure This is the next escalation on the war against the Travis unit tests failing (which came into being by yours truly). By accident, I could recreate the unit test failure on my development machine. This led to a more directed effort to squash the bug. The insight here is that test `(window === undefined)` fails, but `(typeof window === 'undefined`)` succeeds. This undoubtedly has to do with the special status `window` has as a global object. Changes: - Added check on presence of `window` in `Canvas._requestNextFrame()`, fixed local source errors. - Added catch clause in `CanvasRendered._determinePixelRatio()` - small fix: raised timeout for the network `worldCup2014` unit test * Preliminary refactoring in utils.js * Added unit tests for extend routines, commenting and small fixes * More unit tests for extend routines * - Completed unit tests for extend routines in - Small fixes and cleanup in `util.js` - Removed `util.protoExtend()`, not used anywhere * Added unit tests for known font options * Interim save before trying out another proto chain strategy * Fixed problem in first example #3408 * Removed silly file that shouldn't be there * Added unit test for multi-fonts * Comment edits * Verufy unit tests, small adjustments for groups * Further work on getting unit tests to work. PARTS NEED TO BE CLEANED UP! * Further tweaks to get unit tests working * All unit tests passing * Fixes due to linting * Small edits * Removed prototype handling from font pile * Fixes during testing examples of #3408 * Added unit test for edge labels, small fixes * Added unit tests for shorthand string fonts; some tests still failing * All unit tests pass * Removed Label.parseOptions() * Completed shorthand font tests, code cleanup, fixed choosify for edges * Addressed review comments * Addressed review comments, cleanup
7 years ago
9 years ago
  1. var util = require('../../../util');
  2. var Label = require('./shared/Label').default;
  3. var ComponentUtil = require('./shared/ComponentUtil').default;
  4. var CubicBezierEdge = require('./edges/CubicBezierEdge').default;
  5. var BezierEdgeDynamic = require('./edges/BezierEdgeDynamic').default;
  6. var BezierEdgeStatic = require('./edges/BezierEdgeStatic').default;
  7. var StraightEdge = require('./edges/StraightEdge').default;
  8. /**
  9. * An edge connects two nodes and has a specific direction.
  10. */
  11. class Edge {
  12. /**
  13. * @param {Object} options values specific to this edge, must contain at least 'from' and 'to'
  14. * @param {Object} body shared state from Network instance
  15. * @param {Object} globalOptions options from the EdgesHandler instance
  16. * @param {Object} defaultOptions default options from the EdgeHandler instance. Value and reference are constant
  17. */
  18. constructor(options, body, globalOptions, defaultOptions) {
  19. if (body === undefined) {
  20. throw new Error("No body provided");
  21. }
  22. // Since globalOptions is constant in values as well as reference,
  23. // Following needs to be done only once.
  24. this.options = util.bridgeObject(globalOptions);
  25. this.globalOptions = globalOptions;
  26. this.defaultOptions = defaultOptions;
  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.baseWidth = this.options.width;
  36. this.baseFontSize = this.options.font.size;
  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, true /* It's an edge label */);
  42. this.setOptions(options);
  43. }
  44. /**
  45. * Set or overwrite options for the edge
  46. * @param {Object} options an object with options
  47. * @returns {null|boolean} null if no options, boolean if date changed
  48. */
  49. setOptions(options) {
  50. if (!options) {
  51. return;
  52. }
  53. Edge.parseOptions(this.options, options, true, this.globalOptions);
  54. if (options.id !== undefined) {
  55. this.id = options.id;
  56. }
  57. if (options.from !== undefined) {
  58. this.fromId = options.from;
  59. }
  60. if (options.to !== undefined) {
  61. this.toId = options.to;
  62. }
  63. if (options.title !== undefined) {
  64. this.title = options.title;
  65. }
  66. if (options.value !== undefined) {
  67. options.value = parseFloat(options.value);
  68. }
  69. let pile = [options, this.options, this.defaultOptions];
  70. this.chooser = ComponentUtil.choosify('edge', pile);
  71. // update label Module
  72. this.updateLabelModule(options);
  73. let dataChanged = this.updateEdgeType();
  74. // if anything has been updates, reset the selection width and the hover width
  75. this._setInteractionWidths();
  76. // A node is connected when it has a from and to node that both exist in the network.body.nodes.
  77. this.connect();
  78. if (options.hidden !== undefined || options.physics !== undefined) {
  79. dataChanged = true;
  80. }
  81. return dataChanged;
  82. }
  83. /**
  84. *
  85. * @param {Object} parentOptions
  86. * @param {Object} newOptions
  87. * @param {boolean} [allowDeletion=false]
  88. * @param {Object} [globalOptions={}]
  89. * @param {boolean} [copyFromGlobals=false]
  90. */
  91. static parseOptions(parentOptions, newOptions, allowDeletion = false, globalOptions = {}, copyFromGlobals = false) {
  92. var fields = [
  93. 'arrowStrikethrough',
  94. 'id',
  95. 'from',
  96. 'hidden',
  97. 'hoverWidth',
  98. 'labelHighlightBold',
  99. 'length',
  100. 'line',
  101. 'opacity',
  102. 'physics',
  103. 'scaling',
  104. 'selectionWidth',
  105. 'selfReferenceSize',
  106. 'to',
  107. 'title',
  108. 'value',
  109. 'width',
  110. 'font',
  111. 'chosen',
  112. 'widthConstraint'
  113. ];
  114. // only deep extend the items in the field array. These do not have shorthand.
  115. util.selectiveDeepExtend(fields, parentOptions, newOptions, allowDeletion);
  116. // Only copy label if it's a legal value.
  117. if (ComponentUtil.isValidLabel(newOptions.label)) {
  118. parentOptions.label = newOptions.label;
  119. } else {
  120. parentOptions.label = undefined;
  121. }
  122. util.mergeOptions(parentOptions, newOptions, 'smooth', globalOptions);
  123. util.mergeOptions(parentOptions, newOptions, 'shadow', globalOptions);
  124. if (newOptions.dashes !== undefined && newOptions.dashes !== null) {
  125. parentOptions.dashes = newOptions.dashes;
  126. }
  127. else if (allowDeletion === true && newOptions.dashes === null) {
  128. parentOptions.dashes = Object.create(globalOptions.dashes); // this sets the pointer of the option back to the global option.
  129. }
  130. // set the scaling newOptions
  131. if (newOptions.scaling !== undefined && newOptions.scaling !== null) {
  132. if (newOptions.scaling.min !== undefined) {parentOptions.scaling.min = newOptions.scaling.min;}
  133. if (newOptions.scaling.max !== undefined) {parentOptions.scaling.max = newOptions.scaling.max;}
  134. util.mergeOptions(parentOptions.scaling, newOptions.scaling, 'label', globalOptions.scaling);
  135. }
  136. else if (allowDeletion === true && newOptions.scaling === null) {
  137. parentOptions.scaling = Object.create(globalOptions.scaling); // this sets the pointer of the option back to the global option.
  138. }
  139. // handle multiple input cases for arrows
  140. if (newOptions.arrows !== undefined && newOptions.arrows !== null) {
  141. if (typeof newOptions.arrows === 'string') {
  142. let arrows = newOptions.arrows.toLowerCase();
  143. parentOptions.arrows.to.enabled = arrows.indexOf("to") != -1;
  144. parentOptions.arrows.middle.enabled = arrows.indexOf("middle") != -1;
  145. parentOptions.arrows.from.enabled = arrows.indexOf("from") != -1;
  146. }
  147. else if (typeof newOptions.arrows === 'object') {
  148. util.mergeOptions(parentOptions.arrows, newOptions.arrows, 'to', globalOptions.arrows);
  149. util.mergeOptions(parentOptions.arrows, newOptions.arrows, 'middle', globalOptions.arrows);
  150. util.mergeOptions(parentOptions.arrows, newOptions.arrows, 'from', globalOptions.arrows);
  151. }
  152. else {
  153. throw new Error("The arrow newOptions can only be an object or a string. Refer to the documentation. You used:" + JSON.stringify(newOptions.arrows));
  154. }
  155. }
  156. else if (allowDeletion === true && newOptions.arrows === null) {
  157. parentOptions.arrows = Object.create(globalOptions.arrows); // this sets the pointer of the option back to the global option.
  158. }
  159. // handle multiple input cases for color
  160. if (newOptions.color !== undefined && newOptions.color !== null) {
  161. let fromColor = newOptions.color;
  162. let toColor = parentOptions.color;
  163. // If passed, fill in values from default options - required in the case of no prototype bridging
  164. if (copyFromGlobals) {
  165. util.deepExtend(toColor, globalOptions.color, false, allowDeletion);
  166. } else {
  167. // Clear local properties - need to do it like this in order to retain prototype bridges
  168. for (var i in toColor) {
  169. if (toColor.hasOwnProperty(i)) {
  170. delete toColor[i];
  171. }
  172. }
  173. }
  174. if (util.isString(toColor)) {
  175. toColor.color = toColor;
  176. toColor.highlight = toColor;
  177. toColor.hover = toColor;
  178. toColor.inherit = false;
  179. if (fromColor.opacity === undefined) {
  180. toColor.opacity = 1.0; // set default
  181. }
  182. }
  183. else {
  184. let colorsDefined = false;
  185. if (fromColor.color !== undefined) {toColor.color = fromColor.color; colorsDefined = true;}
  186. if (fromColor.highlight !== undefined) {toColor.highlight = fromColor.highlight; colorsDefined = true;}
  187. if (fromColor.hover !== undefined) {toColor.hover = fromColor.hover; colorsDefined = true;}
  188. if (fromColor.inherit !== undefined) {toColor.inherit = fromColor.inherit;}
  189. if (fromColor.opacity !== undefined) {toColor.opacity = Math.min(1,Math.max(0,fromColor.opacity));}
  190. if (colorsDefined === true) {
  191. toColor.inherit = false;
  192. } else {
  193. if (toColor.inherit === undefined) {
  194. toColor.inherit = 'from'; // Set default
  195. }
  196. }
  197. }
  198. }
  199. else if (allowDeletion === true && newOptions.color === null) {
  200. parentOptions.color = util.bridgeObject(globalOptions.color); // set the object back to the global options
  201. }
  202. if (allowDeletion === true && newOptions.font === null) {
  203. parentOptions.font = util.bridgeObject(globalOptions.font); // set the object back to the global options
  204. }
  205. }
  206. /**
  207. *
  208. * @returns {{toArrow: boolean, toArrowScale: (allOptions.edges.arrows.to.scaleFactor|{number}|allOptions.edges.arrows.middle.scaleFactor|allOptions.edges.arrows.from.scaleFactor|Array|number), toArrowType: *, middleArrow: boolean, middleArrowScale: (number|allOptions.edges.arrows.middle.scaleFactor|{number}|Array), middleArrowType: (allOptions.edges.arrows.middle.type|{string}|string|*), fromArrow: boolean, fromArrowScale: (allOptions.edges.arrows.to.scaleFactor|{number}|allOptions.edges.arrows.middle.scaleFactor|allOptions.edges.arrows.from.scaleFactor|Array|number), fromArrowType: *, arrowStrikethrough: (*|boolean|allOptions.edges.arrowStrikethrough|{boolean}), color: undefined, inheritsColor: (string|string|string|allOptions.edges.color.inherit|{string, boolean}|Array|*), opacity: *, hidden: *, length: *, shadow: *, shadowColor: *, shadowSize: *, shadowX: *, shadowY: *, dashes: (*|boolean|Array|allOptions.edges.dashes|{boolean, array}), width: *}}
  209. */
  210. getFormattingValues() {
  211. let toArrow = (this.options.arrows.to === true) || (this.options.arrows.to.enabled === true)
  212. let fromArrow = (this.options.arrows.from === true) || (this.options.arrows.from.enabled === true)
  213. let middleArrow = (this.options.arrows.middle === true) || (this.options.arrows.middle.enabled === true)
  214. let inheritsColor = this.options.color.inherit;
  215. let values = {
  216. toArrow: toArrow,
  217. toArrowScale: this.options.arrows.to.scaleFactor,
  218. toArrowType: this.options.arrows.to.type,
  219. middleArrow: middleArrow,
  220. middleArrowScale: this.options.arrows.middle.scaleFactor,
  221. middleArrowType: this.options.arrows.middle.type,
  222. fromArrow: fromArrow,
  223. fromArrowScale: this.options.arrows.from.scaleFactor,
  224. fromArrowType: this.options.arrows.from.type,
  225. arrowStrikethrough: this.options.arrowStrikethrough,
  226. color: (inheritsColor? undefined : this.options.color.color),
  227. inheritsColor: inheritsColor,
  228. opacity: this.options.color.opacity,
  229. hidden: this.options.hidden,
  230. length: this.options.length,
  231. shadow: this.options.shadow.enabled,
  232. shadowColor: this.options.shadow.color,
  233. shadowSize: this.options.shadow.size,
  234. shadowX: this.options.shadow.x,
  235. shadowY: this.options.shadow.y,
  236. dashes: this.options.dashes,
  237. width: this.options.width
  238. };
  239. if (this.selected || this.hover) {
  240. if (this.chooser === true) {
  241. if (this.selected) {
  242. let selectedWidth = this.options.selectionWidth;
  243. if (typeof selectedWidth === 'function') {
  244. values.width = selectedWidth(values.width);
  245. } else if (typeof selectedWidth === 'number') {
  246. values.width += selectedWidth;
  247. }
  248. values.width = Math.max(values.width, 0.3 / this.body.view.scale);
  249. values.color = this.options.color.highlight;
  250. values.shadow = this.options.shadow.enabled;
  251. } else if (this.hover) {
  252. let hoverWidth = this.options.hoverWidth;
  253. if (typeof hoverWidth === 'function') {
  254. values.width = hoverWidth(values.width);
  255. } else if (typeof hoverWidth === 'number') {
  256. values.width += hoverWidth;
  257. }
  258. values.width = Math.max(values.width, 0.3 / this.body.view.scale);
  259. values.color = this.options.color.hover;
  260. values.shadow = this.options.shadow.enabled;
  261. }
  262. } else if (typeof this.chooser === 'function') {
  263. this.chooser(values, this.options.id, this.selected, this.hover);
  264. if (values.color !== undefined) {
  265. values.inheritsColor = false;
  266. }
  267. if (values.shadow === false) {
  268. if ((values.shadowColor !== this.options.shadow.color) ||
  269. (values.shadowSize !== this.options.shadow.size) ||
  270. (values.shadowX !== this.options.shadow.x) ||
  271. (values.shadowY !== this.options.shadow.y)) {
  272. values.shadow = true;
  273. }
  274. }
  275. }
  276. } else {
  277. values.shadow = this.options.shadow.enabled;
  278. values.width = Math.max(values.width, 0.3 / this.body.view.scale);
  279. }
  280. return values;
  281. }
  282. /**
  283. * update the options in the label module
  284. *
  285. * @param {Object} options
  286. */
  287. updateLabelModule(options) {
  288. let pile = [
  289. options,
  290. this.options,
  291. this.globalOptions, // Currently set global edge options
  292. this.defaultOptions
  293. ];
  294. this.labelModule.update(this.options, pile);
  295. if (this.labelModule.baseSize !== undefined) {
  296. this.baseFontSize = this.labelModule.baseSize;
  297. }
  298. }
  299. /**
  300. * update the edge type, set the options
  301. * @returns {boolean}
  302. */
  303. updateEdgeType() {
  304. let smooth = this.options.smooth;
  305. let dataChanged = false;
  306. let changeInType = true;
  307. if (this.edgeType !== undefined) {
  308. if ((((this.edgeType instanceof BezierEdgeDynamic) &&
  309. (smooth.enabled === true) &&
  310. (smooth.type === 'dynamic'))) ||
  311. (((this.edgeType instanceof CubicBezierEdge) &&
  312. (smooth.enabled === true) &&
  313. (smooth.type === 'cubicBezier'))) ||
  314. (((this.edgeType instanceof BezierEdgeStatic) &&
  315. (smooth.enabled === true) &&
  316. (smooth.type !== 'dynamic') &&
  317. (smooth.type !== 'cubicBezier'))) ||
  318. (((this.edgeType instanceof StraightEdge) &&
  319. (smooth.type.enabled === false)))) {
  320. changeInType = false;
  321. }
  322. if (changeInType === true) {
  323. dataChanged = this.cleanup();
  324. }
  325. }
  326. if (changeInType === true) {
  327. if (smooth.enabled === true) {
  328. if (smooth.type === 'dynamic') {
  329. dataChanged = true;
  330. this.edgeType = new BezierEdgeDynamic(this.options, this.body, this.labelModule);
  331. } else if (smooth.type === 'cubicBezier') {
  332. this.edgeType = new CubicBezierEdge(this.options, this.body, this.labelModule);
  333. } else {
  334. this.edgeType = new BezierEdgeStatic(this.options, this.body, this.labelModule);
  335. }
  336. } else {
  337. this.edgeType = new StraightEdge(this.options, this.body, this.labelModule);
  338. }
  339. } else { // if nothing changes, we just set the options.
  340. this.edgeType.setOptions(this.options);
  341. }
  342. return dataChanged;
  343. }
  344. /**
  345. * Connect an edge to its nodes
  346. */
  347. connect() {
  348. this.disconnect();
  349. this.from = this.body.nodes[this.fromId] || undefined;
  350. this.to = this.body.nodes[this.toId] || undefined;
  351. this.connected = (this.from !== undefined && this.to !== undefined);
  352. if (this.connected === true) {
  353. this.from.attachEdge(this);
  354. this.to.attachEdge(this);
  355. }
  356. else {
  357. if (this.from) {
  358. this.from.detachEdge(this);
  359. }
  360. if (this.to) {
  361. this.to.detachEdge(this);
  362. }
  363. }
  364. this.edgeType.connect();
  365. }
  366. /**
  367. * Disconnect an edge from its nodes
  368. */
  369. disconnect() {
  370. if (this.from) {
  371. this.from.detachEdge(this);
  372. this.from = undefined;
  373. }
  374. if (this.to) {
  375. this.to.detachEdge(this);
  376. this.to = undefined;
  377. }
  378. this.connected = false;
  379. }
  380. /**
  381. * get the title of this edge.
  382. * @return {string} title The title of the edge, or undefined when no title
  383. * has been set.
  384. */
  385. getTitle() {
  386. return this.title;
  387. }
  388. /**
  389. * check if this node is selecte
  390. * @return {boolean} selected True if node is selected, else false
  391. */
  392. isSelected() {
  393. return this.selected;
  394. }
  395. /**
  396. * Retrieve the value of the edge. Can be undefined
  397. * @return {number} value
  398. */
  399. getValue() {
  400. return this.options.value;
  401. }
  402. /**
  403. * Adjust the value range of the edge. The edge will adjust it's width
  404. * based on its value.
  405. * @param {number} min
  406. * @param {number} max
  407. * @param {number} total
  408. */
  409. setValueRange(min, max, total) {
  410. if (this.options.value !== undefined) {
  411. var scale = this.options.scaling.customScalingFunction(min, max, total, this.options.value);
  412. var widthDiff = this.options.scaling.max - this.options.scaling.min;
  413. if (this.options.scaling.label.enabled === true) {
  414. var fontDiff = this.options.scaling.label.max - this.options.scaling.label.min;
  415. this.options.font.size = this.options.scaling.label.min + scale * fontDiff;
  416. }
  417. this.options.width = this.options.scaling.min + scale * widthDiff;
  418. }
  419. else {
  420. this.options.width = this.baseWidth;
  421. this.options.font.size = this.baseFontSize;
  422. }
  423. this._setInteractionWidths();
  424. this.updateLabelModule();
  425. }
  426. /**
  427. *
  428. * @private
  429. */
  430. _setInteractionWidths() {
  431. if (typeof this.options.hoverWidth === 'function') {
  432. this.edgeType.hoverWidth = this.options.hoverWidth(this.options.width);
  433. } else {
  434. this.edgeType.hoverWidth = this.options.hoverWidth + this.options.width;
  435. }
  436. if (typeof this.options.selectionWidth === 'function') {
  437. this.edgeType.selectionWidth = this.options.selectionWidth(this.options.width);
  438. } else {
  439. this.edgeType.selectionWidth = this.options.selectionWidth + this.options.width;
  440. }
  441. }
  442. /**
  443. * Redraw a edge
  444. * Draw this edge in the given canvas
  445. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  446. * @param {CanvasRenderingContext2D} ctx
  447. */
  448. draw(ctx) {
  449. let values = this.getFormattingValues();
  450. if (values.hidden) {
  451. return;
  452. }
  453. // get the via node from the edge type
  454. let viaNode = this.edgeType.getViaNode();
  455. let arrowData = {};
  456. // restore edge targets to defaults
  457. this.edgeType.fromPoint = this.edgeType.from;
  458. this.edgeType.toPoint = this.edgeType.to;
  459. // from and to arrows give a different end point for edges. we set them here
  460. if (values.fromArrow) {
  461. arrowData.from = this.edgeType.getArrowData(ctx, 'from', viaNode, this.selected, this.hover, values);
  462. if (values.arrowStrikethrough === false)
  463. this.edgeType.fromPoint = arrowData.from.core;
  464. }
  465. if (values.toArrow) {
  466. arrowData.to = this.edgeType.getArrowData(ctx, 'to', viaNode, this.selected, this.hover, values);
  467. if (values.arrowStrikethrough === false)
  468. this.edgeType.toPoint = arrowData.to.core;
  469. }
  470. // the middle arrow depends on the line, which can depend on the to and from arrows so we do this one lastly.
  471. if (values.middleArrow) {
  472. arrowData.middle = this.edgeType.getArrowData(ctx,'middle', viaNode, this.selected, this.hover, values);
  473. }
  474. // draw everything
  475. this.edgeType.drawLine(ctx, values, this.selected, this.hover, viaNode);
  476. this.drawArrows(ctx, arrowData, values);
  477. this.drawLabel(ctx, viaNode);
  478. }
  479. /**
  480. *
  481. * @param {CanvasRenderingContext2D} ctx
  482. * @param {Object} arrowData
  483. * @param {{toArrow: boolean, toArrowScale: (allOptions.edges.arrows.to.scaleFactor|{number}|allOptions.edges.arrows.middle.scaleFactor|allOptions.edges.arrows.from.scaleFactor|Array|number), toArrowType: *, middleArrow: boolean, middleArrowScale: (number|allOptions.edges.arrows.middle.scaleFactor|{number}|Array), middleArrowType: (allOptions.edges.arrows.middle.type|{string}|string|*), fromArrow: boolean, fromArrowScale: (allOptions.edges.arrows.to.scaleFactor|{number}|allOptions.edges.arrows.middle.scaleFactor|allOptions.edges.arrows.from.scaleFactor|Array|number), fromArrowType: *, arrowStrikethrough: (*|boolean|allOptions.edges.arrowStrikethrough|{boolean}), color: undefined, inheritsColor: (string|string|string|allOptions.edges.color.inherit|{string, boolean}|Array|*), opacity: *, hidden: *, length: *, shadow: *, shadowColor: *, shadowSize: *, shadowX: *, shadowY: *, dashes: (*|boolean|Array|allOptions.edges.dashes|{boolean, array}), width: *}} values
  484. */
  485. drawArrows(ctx, arrowData, values) {
  486. if (values.fromArrow) {
  487. this.edgeType.drawArrowHead(ctx, values, this.selected, this.hover, arrowData.from);
  488. }
  489. if (values.middleArrow) {
  490. this.edgeType.drawArrowHead(ctx, values, this.selected, this.hover, arrowData.middle);
  491. }
  492. if (values.toArrow) {
  493. this.edgeType.drawArrowHead(ctx, values, this.selected, this.hover, arrowData.to);
  494. }
  495. }
  496. /**
  497. *
  498. * @param {CanvasRenderingContext2D} ctx
  499. * @param {Node} viaNode
  500. */
  501. drawLabel(ctx, viaNode) {
  502. if (this.options.label !== undefined) {
  503. // set style
  504. var node1 = this.from;
  505. var node2 = this.to;
  506. if (this.labelModule.differentState(this.selected, this.hover)) {
  507. this.labelModule.getTextSize(ctx, this.selected, this.hover);
  508. }
  509. if (node1.id != node2.id) {
  510. this.labelModule.pointToSelf = false;
  511. var point = this.edgeType.getPoint(0.5, viaNode);
  512. ctx.save();
  513. let rotationPoint = this._getRotation(ctx);
  514. if (rotationPoint.angle != 0) {
  515. ctx.translate(rotationPoint.x, rotationPoint.y);
  516. ctx.rotate(rotationPoint.angle);
  517. }
  518. // draw the label
  519. this.labelModule.draw(ctx, point.x, point.y, this.selected, this.hover);
  520. /*
  521. // Useful debug code: draw a border around the label
  522. // This should **not** be enabled in production!
  523. var size = this.labelModule.getSize();; // ;; intentional so lint catches it
  524. ctx.strokeStyle = "#ff0000";
  525. ctx.strokeRect(size.left, size.top, size.width, size.height);
  526. // End debug code
  527. */
  528. ctx.restore();
  529. }
  530. else {
  531. // Ignore the orientations.
  532. this.labelModule.pointToSelf = true;
  533. var x, y;
  534. var radius = this.options.selfReferenceSize;
  535. if (node1.shape.width > node1.shape.height) {
  536. x = node1.x + node1.shape.width * 0.5;
  537. y = node1.y - radius;
  538. }
  539. else {
  540. x = node1.x + radius;
  541. y = node1.y - node1.shape.height * 0.5;
  542. }
  543. point = this._pointOnCircle(x, y, radius, 0.125);
  544. this.labelModule.draw(ctx, point.x, point.y, this.selected, this.hover);
  545. }
  546. }
  547. }
  548. /**
  549. * Determine all visual elements of this edge instance, in which the given
  550. * point falls within the bounding shape.
  551. *
  552. * @param {point} point
  553. * @returns {Array.<edgeClickItem|edgeLabelClickItem>} list with the items which are on the point
  554. */
  555. getItemsOnPoint(point) {
  556. var ret = [];
  557. if (this.labelModule.visible()) {
  558. let rotationPoint = this._getRotation();
  559. if (ComponentUtil.pointInRect(this.labelModule.getSize(), point, rotationPoint)) {
  560. ret.push({edgeId:this.id, labelId:0});
  561. }
  562. }
  563. let obj = {
  564. left: point.x,
  565. top: point.y
  566. };
  567. if (this.isOverlappingWith(obj)) {
  568. ret.push({edgeId:this.id});
  569. }
  570. return ret;
  571. }
  572. /**
  573. * Check if this object is overlapping with the provided object
  574. * @param {Object} obj an object with parameters left, top
  575. * @return {boolean} True if location is located on the edge
  576. */
  577. isOverlappingWith(obj) {
  578. if (this.connected) {
  579. var distMax = 10;
  580. var xFrom = this.from.x;
  581. var yFrom = this.from.y;
  582. var xTo = this.to.x;
  583. var yTo = this.to.y;
  584. var xObj = obj.left;
  585. var yObj = obj.top;
  586. var dist = this.edgeType.getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj);
  587. return (dist < distMax);
  588. }
  589. else {
  590. return false
  591. }
  592. }
  593. /**
  594. * Determine the rotation point, if any.
  595. *
  596. * @param {CanvasRenderingContext2D} [ctx] if passed, do a recalculation of the label size
  597. * @returns {rotationPoint} the point to rotate around and the angle in radians to rotate
  598. * @private
  599. */
  600. _getRotation(ctx) {
  601. let viaNode = this.edgeType.getViaNode();
  602. let point = this.edgeType.getPoint(0.5, viaNode);
  603. if (ctx !== undefined) {
  604. this.labelModule.calculateLabelSize(ctx, this.selected, this.hover, point.x, point.y);
  605. }
  606. let ret = {
  607. x: point.x,
  608. y: this.labelModule.size.yLine,
  609. angle: 0
  610. };
  611. if (!this.labelModule.visible()) {
  612. return ret; // Don't even bother doing the atan2, there's nothing to draw
  613. }
  614. if (this.options.font.align === "horizontal") {
  615. return ret; // No need to calculate angle
  616. }
  617. var dy = this.from.y - this.to.y;
  618. var dx = this.from.x - this.to.x;
  619. var angle = Math.atan2(dy, dx); // radians
  620. // rotate so that label is readable
  621. if ((angle < -1 && dx < 0) || (angle > 0 && dx < 0)) {
  622. angle += Math.PI;
  623. }
  624. ret.angle = angle;
  625. return ret;
  626. }
  627. /**
  628. * Get a point on a circle
  629. * @param {number} x
  630. * @param {number} y
  631. * @param {number} radius
  632. * @param {number} percentage Value between 0 (line start) and 1 (line end)
  633. * @return {Object} point
  634. * @private
  635. */
  636. _pointOnCircle(x, y, radius, percentage) {
  637. var angle = percentage * 2 * Math.PI;
  638. return {
  639. x: x + radius * Math.cos(angle),
  640. y: y - radius * Math.sin(angle)
  641. }
  642. }
  643. /**
  644. * Sets selected state to true
  645. */
  646. select() {
  647. this.selected = true;
  648. }
  649. /**
  650. * Sets selected state to false
  651. */
  652. unselect() {
  653. this.selected = false;
  654. }
  655. /**
  656. * cleans all required things on delete
  657. * @returns {*}
  658. */
  659. cleanup() {
  660. return this.edgeType.cleanup();
  661. }
  662. /**
  663. * Remove edge from the list and perform necessary cleanup.
  664. */
  665. remove() {
  666. this.cleanup();
  667. this.disconnect();
  668. delete this.body.edges[this.id];
  669. }
  670. /**
  671. * Check if both connecting nodes exist
  672. * @returns {boolean}
  673. */
  674. endPointsValid() {
  675. return this.body.nodes[this.fromId] !== undefined
  676. && this.body.nodes[this.toId] !== undefined;
  677. }
  678. }
  679. export default Edge;