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.

312 lines
10 KiB

9 years ago
9 years ago
9 years ago
  1. let Hammer = require('../../module/hammer');
  2. let hammerUtil = require('../../hammerUtil');
  3. let util = require('../../util');
  4. /**
  5. * Create the main frame for the Network.
  6. * This function is executed once when a Network object is created. The frame
  7. * contains a canvas, and this canvas contains all objects like the axis and
  8. * nodes.
  9. * @private
  10. */
  11. class Canvas {
  12. constructor(body) {
  13. this.body = body;
  14. this.pixelRatio = 1;
  15. this.resizeTimer = undefined;
  16. this.resizeFunction = this._onResize.bind(this);
  17. this.options = {};
  18. this.defaultOptions = {
  19. autoResize: true,
  20. height: '100%',
  21. width: '100%'
  22. };
  23. util.extend(this.options, this.defaultOptions);
  24. this.bindEventListeners();
  25. }
  26. bindEventListeners() {
  27. // bind the events
  28. this.body.emitter.once("resize", (obj) => {
  29. if (obj.width !== 0) {
  30. this.body.view.translation.x = obj.width * 0.5;
  31. }
  32. if (obj.height !== 0) {
  33. this.body.view.translation.y = obj.height * 0.5;
  34. }
  35. });
  36. this.body.emitter.on("setSize", this.setSize.bind(this));
  37. this.body.emitter.on("destroy", () => {
  38. this.hammerFrame.destroy();
  39. this.hammer.destroy();
  40. this._cleanUp();
  41. });
  42. }
  43. setOptions(options) {
  44. if (options !== undefined) {
  45. let fields = ['width','height','autoResize'];
  46. util.selectiveDeepExtend(fields,this.options, options);
  47. }
  48. if (this.options.autoResize === true) {
  49. // automatically adapt to a changing size of the browser.
  50. this._cleanUp();
  51. this.resizeTimer = setInterval(() => {
  52. let changed = this.setSize();
  53. if (changed === true) {
  54. this.body.emitter.emit("_requestRedraw");
  55. }
  56. }, 1000);
  57. this.resizeFunction = this._onResize.bind(this);
  58. util.addEventListener(window,'resize',this.resizeFunction);
  59. }
  60. }
  61. _cleanUp() {
  62. // automatically adapt to a changing size of the browser.
  63. if (this.resizeTimer !== undefined) {
  64. clearInterval(this.resizeTimer);
  65. }
  66. util.removeEventListener(window,'resize',this.resizeFunction);
  67. this.resizeFunction = undefined;
  68. }
  69. _onResize() {
  70. this.setSize();
  71. this.body.emitter.emit("_redraw");
  72. }
  73. _prepareValue(value) {
  74. if (typeof value === 'number') {
  75. return value + 'px';
  76. }
  77. else if (typeof value === 'string') {
  78. if (value.indexOf('%') !== -1 || value.indexOf('px') !== -1) {
  79. return value;
  80. }
  81. else if (value.indexOf('%') === -1) {
  82. return value + 'px';
  83. }
  84. }
  85. throw new Error('Could not use the value supplie for width or height:' + value);
  86. }
  87. /**
  88. * Create the HTML
  89. */
  90. _create() {
  91. // remove all elements from the container element.
  92. while (this.body.container.hasChildNodes()) {
  93. this.body.container.removeChild(this.body.container.firstChild);
  94. }
  95. this.frame = document.createElement('div');
  96. this.frame.className = 'vis-network';
  97. this.frame.style.position = 'relative';
  98. this.frame.style.overflow = 'hidden';
  99. this.frame.tabIndex = 900; // tab index is required for keycharm to bind keystrokes to the div instead of the window
  100. //////////////////////////////////////////////////////////////////
  101. this.frame.canvas = document.createElement("canvas");
  102. this.frame.canvas.style.position = 'relative';
  103. this.frame.appendChild(this.frame.canvas);
  104. if (!this.frame.canvas.getContext) {
  105. let noCanvas = document.createElement( 'DIV' );
  106. noCanvas.style.color = 'red';
  107. noCanvas.style.fontWeight = 'bold' ;
  108. noCanvas.style.padding = '10px';
  109. noCanvas.innerHTML = 'Error: your browser does not support HTML canvas';
  110. this.frame.canvas.appendChild(noCanvas);
  111. }
  112. else {
  113. let ctx = this.frame.canvas.getContext("2d");
  114. this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio ||
  115. ctx.mozBackingStorePixelRatio ||
  116. ctx.msBackingStorePixelRatio ||
  117. ctx.oBackingStorePixelRatio ||
  118. ctx.backingStorePixelRatio || 1);
  119. this.frame.canvas.getContext("2d").setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0);
  120. }
  121. // add the frame to the container element
  122. this.body.container.appendChild(this.frame);
  123. this.body.view.scale = 1;
  124. this.body.view.translation = {x: 0.5 * this.frame.canvas.clientWidth,y: 0.5 * this.frame.canvas.clientHeight};
  125. this._bindHammer();
  126. }
  127. /**
  128. * This function binds hammer, it can be repeated over and over due to the uniqueness check.
  129. * @private
  130. */
  131. _bindHammer() {
  132. if (this.hammer !== undefined) {
  133. this.hammer.destroy();
  134. }
  135. this.drag = {};
  136. this.pinch = {};
  137. // init hammer
  138. this.hammer = new Hammer(this.frame.canvas);
  139. this.hammer.get('pinch').set({enable: true});
  140. // enable to get better response, todo: test on mobile.
  141. this.hammer.get('pan').set({threshold:5, direction:30}); // 30 is ALL_DIRECTIONS in hammer.
  142. hammerUtil.onTouch(this.hammer, (event) => {this.body.eventListeners.onTouch(event)});
  143. this.hammer.on('tap', (event) => {this.body.eventListeners.onTap(event)});
  144. this.hammer.on('doubletap', (event) => {this.body.eventListeners.onDoubleTap(event)});
  145. this.hammer.on('press', (event) => {this.body.eventListeners.onHold(event)});
  146. this.hammer.on('panstart', (event) => {this.body.eventListeners.onDragStart(event)});
  147. this.hammer.on('panmove', (event) => {this.body.eventListeners.onDrag(event)});
  148. this.hammer.on('panend', (event) => {this.body.eventListeners.onDragEnd(event)});
  149. this.hammer.on('pinch', (event) => {this.body.eventListeners.onPinch(event)});
  150. // TODO: neatly cleanup these handlers when re-creating the Canvas, IF these are done with hammer, event.stopPropagation will not work?
  151. this.frame.canvas.addEventListener('mousewheel', (event) => {this.body.eventListeners.onMouseWheel(event)});
  152. this.frame.canvas.addEventListener('DOMMouseScroll', (event) => {this.body.eventListeners.onMouseWheel(event)});
  153. this.frame.canvas.addEventListener('mousemove', (event) => {this.body.eventListeners.onMouseMove(event)});
  154. this.frame.canvas.addEventListener('contextmenu', (event) => {this.body.eventListeners.onContext(event)});
  155. this.hammerFrame = new Hammer(this.frame);
  156. hammerUtil.onRelease(this.hammerFrame, (event) => {this.body.eventListeners.onRelease(event)});
  157. }
  158. /**
  159. * Set a new size for the network
  160. * @param {string} width Width in pixels or percentage (for example '800px'
  161. * or '50%')
  162. * @param {string} height Height in pixels or percentage (for example '400px'
  163. * or '30%')
  164. */
  165. setSize(width = this.options.width, height = this.options.height) {
  166. width = this._prepareValue(width);
  167. height= this._prepareValue(height);
  168. let emitEvent = false;
  169. let oldWidth = this.frame.canvas.width;
  170. let oldHeight = this.frame.canvas.height;
  171. if (width != this.options.width || height != this.options.height || this.frame.style.width != width || this.frame.style.height != height) {
  172. this.frame.style.width = width;
  173. this.frame.style.height = height;
  174. this.frame.canvas.style.width = '100%';
  175. this.frame.canvas.style.height = '100%';
  176. this.frame.canvas.width = Math.round(this.frame.canvas.clientWidth * this.pixelRatio);
  177. this.frame.canvas.height = Math.round(this.frame.canvas.clientHeight * this.pixelRatio);
  178. this.options.width = width;
  179. this.options.height = height;
  180. emitEvent = true;
  181. }
  182. else {
  183. // this would adapt the width of the canvas to the width from 100% if and only if
  184. // there is a change.
  185. if (this.frame.canvas.width != Math.round(this.frame.canvas.clientWidth * this.pixelRatio)) {
  186. this.frame.canvas.width = Math.round(this.frame.canvas.clientWidth * this.pixelRatio);
  187. emitEvent = true;
  188. }
  189. if (this.frame.canvas.height != Math.round(this.frame.canvas.clientHeight * this.pixelRatio)) {
  190. this.frame.canvas.height = Math.round(this.frame.canvas.clientHeight * this.pixelRatio);
  191. emitEvent = true;
  192. }
  193. }
  194. if (emitEvent === true) {
  195. this.body.emitter.emit('resize', {
  196. width:Math.round(this.frame.canvas.width / this.pixelRatio),
  197. height:Math.round(this.frame.canvas.height / this.pixelRatio),
  198. oldWidth: Math.round(oldWidth / this.pixelRatio),
  199. oldHeight: Math.round(oldHeight / this.pixelRatio)
  200. });
  201. }
  202. return emitEvent;
  203. };
  204. /**
  205. * Convert the X coordinate in DOM-space (coordinate point in browser relative to the container div) to
  206. * the X coordinate in canvas-space (the simulation sandbox, which the camera looks upon)
  207. * @param {number} x
  208. * @returns {number}
  209. * @private
  210. */
  211. _XconvertDOMtoCanvas(x) {
  212. return (x - this.body.view.translation.x) / this.body.view.scale;
  213. }
  214. /**
  215. * Convert the X coordinate in canvas-space (the simulation sandbox, which the camera looks upon) to
  216. * the X coordinate in DOM-space (coordinate point in browser relative to the container div)
  217. * @param {number} x
  218. * @returns {number}
  219. * @private
  220. */
  221. _XconvertCanvasToDOM(x) {
  222. return x * this.body.view.scale + this.body.view.translation.x;
  223. }
  224. /**
  225. * Convert the Y coordinate in DOM-space (coordinate point in browser relative to the container div) to
  226. * the Y coordinate in canvas-space (the simulation sandbox, which the camera looks upon)
  227. * @param {number} y
  228. * @returns {number}
  229. * @private
  230. */
  231. _YconvertDOMtoCanvas(y) {
  232. return (y - this.body.view.translation.y) / this.body.view.scale;
  233. }
  234. /**
  235. * Convert the Y coordinate in canvas-space (the simulation sandbox, which the camera looks upon) to
  236. * the Y coordinate in DOM-space (coordinate point in browser relative to the container div)
  237. * @param {number} y
  238. * @returns {number}
  239. * @private
  240. */
  241. _YconvertCanvasToDOM(y) {
  242. return y * this.body.view.scale + this.body.view.translation.y;
  243. }
  244. /**
  245. *
  246. * @param {object} pos = {x: number, y: number}
  247. * @returns {{x: number, y: number}}
  248. * @constructor
  249. */
  250. canvasToDOM (pos) {
  251. return {x: this._XconvertCanvasToDOM(pos.x), y: this._YconvertCanvasToDOM(pos.y)};
  252. }
  253. /**
  254. *
  255. * @param {object} pos = {x: number, y: number}
  256. * @returns {{x: number, y: number}}
  257. * @constructor
  258. */
  259. DOMtoCanvas (pos) {
  260. return {x: this._XconvertDOMtoCanvas(pos.x), y: this._YconvertDOMtoCanvas(pos.y)};
  261. }
  262. }
  263. export default Canvas;