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.

495 lines
16 KiB

9 years ago
9 years ago
9 years ago
  1. var util = require('../../../util');
  2. import Label from './shared/Label'
  3. import Box from './nodes/shapes/Box'
  4. import Circle from './nodes/shapes/Circle'
  5. import CircularImage from './nodes/shapes/CircularImage'
  6. import Database from './nodes/shapes/Database'
  7. import Diamond from './nodes/shapes/Diamond'
  8. import Dot from './nodes/shapes/Dot'
  9. import Ellipse from './nodes/shapes/Ellipse'
  10. import Icon from './nodes/shapes/Icon'
  11. import Image from './nodes/shapes/Image'
  12. import Square from './nodes/shapes/Square'
  13. import Star from './nodes/shapes/Star'
  14. import Text from './nodes/shapes/Text'
  15. import Triangle from './nodes/shapes/Triangle'
  16. import TriangleDown from './nodes/shapes/TriangleDown'
  17. import Validator from "../../../shared/Validator";
  18. import {printStyle} from "../../../shared/Validator";
  19. /**
  20. * @class Node
  21. * A node. A node can be connected to other nodes via one or multiple edges.
  22. * @param {object} options An object containing options for the node. All
  23. * options are optional, except for the id.
  24. * {number} id Id of the node. Required
  25. * {string} label Text label for the node
  26. * {number} x Horizontal position of the node
  27. * {number} y Vertical position of the node
  28. * {string} shape Node shape, available:
  29. * "database", "circle", "ellipse",
  30. * "box", "image", "text", "dot",
  31. * "star", "triangle", "triangleDown",
  32. * "square", "icon"
  33. * {string} image An image url
  34. * {string} title An title text, can be HTML
  35. * {anytype} group A group name or number
  36. * @param {Network.Images} imagelist A list with images. Only needed
  37. * when the node has an image
  38. * @param {Network.Groups} grouplist A list with groups. Needed for
  39. * retrieving group options
  40. * @param {Object} constants An object with default values for
  41. * example for the color
  42. *
  43. */
  44. class Node {
  45. constructor(options, body, imagelist, grouplist, globalOptions) {
  46. this.options = util.bridgeObject(globalOptions);
  47. this.globalOptions = globalOptions;
  48. this.body = body;
  49. this.edges = []; // all edges connected to this node
  50. // set defaults for the options
  51. this.id = undefined;
  52. this.imagelist = imagelist;
  53. this.grouplist = grouplist;
  54. // state options
  55. this._x = undefined;
  56. this._y = undefined;
  57. this.baseSize = this.options.size;
  58. this.baseFontSize = this.options.font.size;
  59. this.predefinedPosition = false; // used to check if initial fit should just take the range or approximate
  60. this.selected = false;
  61. this.hover = false;
  62. this.labelModule = new Label(this.body, this.options);
  63. this.setOptions(options);
  64. }
  65. get x() {
  66. return this._x;
  67. }
  68. set x(newX) {
  69. this._x = newX;
  70. this.body.emitter.emit('_positionUpdate', {id: this.id, x: this._x, y: this._y});
  71. }
  72. /**
  73. * Non emitting version for use by physics engine so we don't create infinite loops.
  74. * @param newX
  75. */
  76. setX(newX) {
  77. this._x = newX;
  78. }
  79. get y() {
  80. return this._y;
  81. }
  82. set y(newY) {
  83. this._y = newY;
  84. this.body.emitter.emit('_positionUpdate', {id: this.id, x: this._x, y: this._y});
  85. }
  86. /**
  87. * Non emitting version for use by physics engine so we don't create infinite loops.
  88. * @param newY
  89. */
  90. setY(newY) {
  91. this._y = newY;
  92. }
  93. /**
  94. * Attach a edge to the node
  95. * @param {Edge} edge
  96. */
  97. attachEdge(edge) {
  98. if (this.edges.indexOf(edge) === -1) {
  99. this.edges.push(edge);
  100. }
  101. }
  102. /**
  103. * Detach a edge from the node
  104. * @param {Edge} edge
  105. */
  106. detachEdge(edge) {
  107. var index = this.edges.indexOf(edge);
  108. if (index != -1) {
  109. this.edges.splice(index, 1);
  110. }
  111. }
  112. /**
  113. * Set or overwrite options for the node
  114. * @param {Object} options an object with options
  115. * @param {Object} constants and object with default, global options
  116. */
  117. setOptions(options) {
  118. let currentShape = this.options.shape;
  119. if (!options) {
  120. return;
  121. }
  122. // basic options
  123. if (options.id !== undefined) {this.id = options.id;}
  124. if (this.id === undefined) {
  125. throw "Node must have an id";
  126. }
  127. // set these options locally
  128. // clear x and y positions
  129. if (options.x !== undefined) {
  130. if (options.x === null) {this.x = undefined; this.predefinedPosition = false;}
  131. else {this.x = parseInt(options.x); this.predefinedPosition = true;}
  132. }
  133. if (options.y !== undefined) {
  134. if (options.y === null) {this.y = undefined; this.predefinedPosition = false;}
  135. else {this.y = parseInt(options.y); this.predefinedPosition = true;}
  136. }
  137. if (options.size !== undefined) {this.baseSize = options.size;}
  138. if (options.value !== undefined) {options.value = parseFloat(options.value);}
  139. // copy group options
  140. if (typeof options.group === 'number' || (typeof options.group === 'string' && options.group != '')) {
  141. var groupObj = this.grouplist.get(options.group);
  142. util.deepExtend(this.options, groupObj);
  143. // the color object needs to be completely defined. Since groups can partially overwrite the colors, we parse it again, just in case.
  144. this.options.color = util.parseColor(this.options.color);
  145. }
  146. // this transforms all shorthands into fully defined options
  147. this.parseOptions(this.options, options, true, this.globalOptions);
  148. // load the images
  149. if (this.options.image !== undefined) {
  150. if (this.imagelist) {
  151. this.imageObj = this.imagelist.load(this.options.image, this.options.brokenImage, this.id);
  152. }
  153. else {
  154. throw "No imagelist provided";
  155. }
  156. }
  157. this.updateLabelModule();
  158. this.updateShape(currentShape);
  159. if (options.mass !== undefined) {
  160. this.options.mass = options.mass;
  161. this.body.emitter.emit('_physicsUpdate', {id: this.id, options: {mass: options.mass}});
  162. }
  163. if (options.physics !== undefined) {
  164. this.options.physics = options.physics;
  165. this.body.emitter.emit('_physicsUpdate', {id: this.id, options: {physics: options.physics}});
  166. }
  167. if (options.hidden !== undefined) {
  168. return true;
  169. }
  170. return false;
  171. }
  172. /**
  173. * This process all possible shorthands in the new options and makes sure that the parentOptions are fully defined.
  174. * Static so it can also be used by the handler.
  175. * @param parentOptions
  176. * @param newOptions
  177. */
  178. parseOptions(parentOptions, newOptions, allowDeletion = false, globalOptions = {}) {
  179. var fields = [
  180. 'color',
  181. 'font',
  182. 'fixed',
  183. 'shadow'
  184. ];
  185. util.selectiveNotDeepExtend(fields, parentOptions, newOptions, allowDeletion);
  186. // merge the shadow options into the parent.
  187. util.mergeOptions(parentOptions, newOptions, 'shadow', allowDeletion, globalOptions);
  188. // individual shape newOptions
  189. if (newOptions.color !== undefined && newOptions.color !== null) {
  190. let parsedColor = util.parseColor(newOptions.color);
  191. util.fillIfDefined(parentOptions.color, parsedColor);
  192. }
  193. else if (allowDeletion === true && newOptions.color === null) {
  194. parentOptions.color = Object.create(globalOptions.color); // this sets the pointer of the option back to the global option.
  195. }
  196. // handle the fixed options
  197. if (newOptions.fixed !== undefined && newOptions.fixed !== null) {
  198. if (typeof newOptions.fixed === 'boolean') {
  199. if (parentOptions.fixed.x !== newOptions.fixed || parentOptions.fixed.y !== newOptions.fixed) {
  200. parentOptions.fixed.x = newOptions.fixed;
  201. parentOptions.fixed.y = newOptions.fixed;
  202. this.body.emitter.emit('_physicsUpdate', {id: this.id, options: {fixed: {x: newOptions.fixed, y: newOptions.fixed}}});
  203. }
  204. }
  205. else {
  206. if (newOptions.fixed.x !== undefined &&
  207. typeof newOptions.fixed.x === 'boolean' &&
  208. parentOptions.fixed.x !== newOptions.fixed.x)
  209. {
  210. parentOptions.fixed.x = newOptions.fixed.x;
  211. this.body.emitter.emit('_physicsUpdate', {id: this.id, options: {fixed: {x: newOptions.fixed.x}}});
  212. }
  213. if (newOptions.fixed.y !== undefined &&
  214. typeof newOptions.fixed.y === 'boolean' &&
  215. parentOptions.fixed.y !== newOptions.fixed.y)
  216. {
  217. parentOptions.fixed.y = newOptions.fixed.y;
  218. this.body.emitter.emit('_physicsUpdate', {id: this.id, options: {fixed: {y: newOptions.fixed.y}}});
  219. }
  220. }
  221. }
  222. // handle the font options
  223. if (newOptions.font !== undefined && newOptions.font !== null) {
  224. Label.parseOptions(parentOptions.font, newOptions);
  225. }
  226. else if (allowDeletion === true && newOptions.font === null) {
  227. parentOptions.font = Object.create(globalOptions.font); // this sets the pointer of the option back to the global option.
  228. }
  229. // handle the scaling options, specifically the label part
  230. if (newOptions.scaling !== undefined) {
  231. util.mergeOptions(parentOptions.scaling, newOptions.scaling, 'label', allowDeletion, globalOptions.scaling);
  232. }
  233. }
  234. updateLabelModule() {
  235. if (this.options.label === undefined || this.options.label === null) {
  236. this.options.label = '';
  237. }
  238. this.labelModule.setOptions(this.options, true);
  239. if (this.labelModule.baseSize !== undefined) {
  240. this.baseFontSize = this.labelModule.baseSize;
  241. }
  242. }
  243. updateShape(currentShape) {
  244. if (currentShape === this.options.shape && this.shape) {
  245. this.shape.setOptions(this.options, this.imageObj);
  246. }
  247. else {
  248. // choose draw method depending on the shape
  249. switch (this.options.shape) {
  250. case 'box':
  251. this.shape = new Box(this.options, this.body, this.labelModule);
  252. break;
  253. case 'circle':
  254. this.shape = new Circle(this.options, this.body, this.labelModule);
  255. break;
  256. case 'circularImage':
  257. this.shape = new CircularImage(this.options, this.body, this.labelModule, this.imageObj);
  258. break;
  259. case 'database':
  260. this.shape = new Database(this.options, this.body, this.labelModule);
  261. break;
  262. case 'diamond':
  263. this.shape = new Diamond(this.options, this.body, this.labelModule);
  264. break;
  265. case 'dot':
  266. this.shape = new Dot(this.options, this.body, this.labelModule);
  267. break;
  268. case 'ellipse':
  269. this.shape = new Ellipse(this.options, this.body, this.labelModule);
  270. break;
  271. case 'icon':
  272. this.shape = new Icon(this.options, this.body, this.labelModule);
  273. break;
  274. case 'image':
  275. this.shape = new Image(this.options, this.body, this.labelModule, this.imageObj);
  276. break;
  277. case 'square':
  278. this.shape = new Square(this.options, this.body, this.labelModule);
  279. break;
  280. case 'star':
  281. this.shape = new Star(this.options, this.body, this.labelModule);
  282. break;
  283. case 'text':
  284. this.shape = new Text(this.options, this.body, this.labelModule);
  285. break;
  286. case 'triangle':
  287. this.shape = new Triangle(this.options, this.body, this.labelModule);
  288. break;
  289. case 'triangleDown':
  290. this.shape = new TriangleDown(this.options, this.body, this.labelModule);
  291. break;
  292. default:
  293. this.shape = new Ellipse(this.options, this.body, this.labelModule);
  294. break;
  295. }
  296. }
  297. this._reset();
  298. }
  299. /**
  300. * select this node
  301. */
  302. select() {
  303. this.selected = true;
  304. this._reset();
  305. }
  306. /**
  307. * unselect this node
  308. */
  309. unselect() {
  310. this.selected = false;
  311. this._reset();
  312. }
  313. /**
  314. * Reset the calculated size of the node, forces it to recalculate its size
  315. * @private
  316. */
  317. _reset() {
  318. this.shape.width = undefined;
  319. this.shape.height = undefined;
  320. }
  321. /**
  322. * get the title of this node.
  323. * @return {string} title The title of the node, or undefined when no title
  324. * has been set.
  325. */
  326. getTitle() {
  327. return this.options.title;
  328. }
  329. /**
  330. * Calculate the distance to the border of the Node
  331. * @param {CanvasRenderingContext2D} ctx
  332. * @param {Number} angle Angle in radians
  333. * @returns {number} distance Distance to the border in pixels
  334. */
  335. distanceToBorder(ctx, angle) {
  336. return this.shape.distanceToBorder(ctx,angle);
  337. }
  338. /**
  339. * Check if this node has a fixed x and y position
  340. * @return {boolean} true if fixed, false if not
  341. */
  342. isFixed() {
  343. return (this.options.fixed.x && this.options.fixed.y);
  344. }
  345. /**
  346. * check if this node is selecte
  347. * @return {boolean} selected True if node is selected, else false
  348. */
  349. isSelected() {
  350. return this.selected;
  351. }
  352. /**
  353. * Retrieve the value of the node. Can be undefined
  354. * @return {Number} value
  355. */
  356. getValue() {
  357. return this.options.value;
  358. }
  359. /**
  360. * Adjust the value range of the node. The node will adjust it's size
  361. * based on its value.
  362. * @param {Number} min
  363. * @param {Number} max
  364. */
  365. setValueRange(min, max, total) {
  366. if (this.options.value !== undefined) {
  367. var scale = this.options.scaling.customScalingFunction(min, max, total, this.options.value);
  368. var sizeDiff = this.options.scaling.max - this.options.scaling.min;
  369. if (this.options.scaling.label.enabled === true) {
  370. var fontDiff = this.options.scaling.label.max - this.options.scaling.label.min;
  371. this.options.font.size = this.options.scaling.label.min + scale * fontDiff;
  372. }
  373. this.options.size = this.options.scaling.min + scale * sizeDiff;
  374. }
  375. else {
  376. this.options.size = this.baseSize;
  377. this.options.font.size = this.baseFontSize;
  378. }
  379. }
  380. /**
  381. * Draw this node in the given canvas
  382. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  383. * @param {CanvasRenderingContext2D} ctx
  384. */
  385. draw(ctx) {
  386. this.shape.draw(ctx, this.x, this.y, this.selected, this.hover);
  387. }
  388. /**
  389. * Update the bounding box of the shape
  390. */
  391. updateBoundingBox(ctx) {
  392. this.shape.updateBoundingBox(this.x,this.y,ctx);
  393. }
  394. /**
  395. * Recalculate the size of this node in the given canvas
  396. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  397. * @param {CanvasRenderingContext2D} ctx
  398. */
  399. resize(ctx) {
  400. this.shape.resize(ctx, this.selected);
  401. }
  402. /**
  403. * Check if this object is overlapping with the provided object
  404. * @param {Object} obj an object with parameters left, top, right, bottom
  405. * @return {boolean} True if location is located on node
  406. */
  407. isOverlappingWith(obj) {
  408. return (
  409. this.shape.left < obj.right &&
  410. this.shape.left + this.shape.width > obj.left &&
  411. this.shape.top < obj.bottom &&
  412. this.shape.top + this.shape.height > obj.top
  413. );
  414. }
  415. /**
  416. * Check if this object is overlapping with the provided object
  417. * @param {Object} obj an object with parameters left, top, right, bottom
  418. * @return {boolean} True if location is located on node
  419. */
  420. isBoundingBoxOverlappingWith(obj) {
  421. return (
  422. this.shape.boundingBox.left < obj.right &&
  423. this.shape.boundingBox.right > obj.left &&
  424. this.shape.boundingBox.top < obj.bottom &&
  425. this.shape.boundingBox.bottom > obj.top
  426. );
  427. }
  428. }
  429. export default Node;