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.

472 lines
15 KiB

9 years ago
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. */
  10. class Canvas {
  11. /**
  12. * @param {Object} body
  13. */
  14. constructor(body) {
  15. this.body = body;
  16. this.pixelRatio = 1;
  17. this.resizeTimer = undefined;
  18. this.resizeFunction = this._onResize.bind(this);
  19. this.cameraState = {};
  20. this.initialized = false;
  21. this.canvasViewCenter = {};
  22. this.options = {};
  23. this.defaultOptions = {
  24. autoResize: true,
  25. height: '100%',
  26. width: '100%'
  27. };
  28. util.extend(this.options, this.defaultOptions);
  29. this.bindEventListeners();
  30. }
  31. /**
  32. * Binds event listeners
  33. */
  34. bindEventListeners() {
  35. // bind the events
  36. this.body.emitter.once("resize", (obj) => {
  37. if (obj.width !== 0) {
  38. this.body.view.translation.x = obj.width * 0.5;
  39. }
  40. if (obj.height !== 0) {
  41. this.body.view.translation.y = obj.height * 0.5;
  42. }
  43. });
  44. this.body.emitter.on("setSize", this.setSize.bind(this));
  45. this.body.emitter.on("destroy", () => {
  46. this.hammerFrame.destroy();
  47. this.hammer.destroy();
  48. this._cleanUp();
  49. });
  50. }
  51. /**
  52. * @param {Object} options
  53. */
  54. setOptions(options) {
  55. if (options !== undefined) {
  56. let fields = ['width','height','autoResize'];
  57. util.selectiveDeepExtend(fields,this.options, options);
  58. }
  59. if (this.options.autoResize === true) {
  60. // automatically adapt to a changing size of the browser.
  61. this._cleanUp();
  62. this.resizeTimer = setInterval(() => {
  63. let changed = this.setSize();
  64. if (changed === true) {
  65. this.body.emitter.emit("_requestRedraw");
  66. }
  67. }, 1000);
  68. this.resizeFunction = this._onResize.bind(this);
  69. util.addEventListener(window,'resize',this.resizeFunction);
  70. }
  71. }
  72. /**
  73. * @private
  74. */
  75. _cleanUp() {
  76. // automatically adapt to a changing size of the browser.
  77. if (this.resizeTimer !== undefined) {
  78. clearInterval(this.resizeTimer);
  79. }
  80. util.removeEventListener(window,'resize',this.resizeFunction);
  81. this.resizeFunction = undefined;
  82. }
  83. /**
  84. * @private
  85. */
  86. _onResize() {
  87. this.setSize();
  88. this.body.emitter.emit("_redraw");
  89. }
  90. /**
  91. * Get and store the cameraState
  92. *
  93. * @param {number} [pixelRatio=this.pixelRatio]
  94. * @private
  95. */
  96. _getCameraState(pixelRatio = this.pixelRatio) {
  97. if (this.initialized === true) {
  98. this.cameraState.previousWidth = this.frame.canvas.width / pixelRatio;
  99. this.cameraState.previousHeight = this.frame.canvas.height / pixelRatio;
  100. this.cameraState.scale = this.body.view.scale;
  101. this.cameraState.position = this.DOMtoCanvas({
  102. x: 0.5 * this.frame.canvas.width / pixelRatio,
  103. y: 0.5 * this.frame.canvas.height / pixelRatio
  104. });
  105. }
  106. }
  107. /**
  108. * Set the cameraState
  109. * @private
  110. */
  111. _setCameraState() {
  112. if (this.cameraState.scale !== undefined &&
  113. this.frame.canvas.clientWidth !== 0 &&
  114. this.frame.canvas.clientHeight !== 0 &&
  115. this.pixelRatio !== 0 &&
  116. this.cameraState.previousWidth > 0) {
  117. let widthRatio = (this.frame.canvas.width / this.pixelRatio) / this.cameraState.previousWidth;
  118. let heightRatio = (this.frame.canvas.height / this.pixelRatio) / this.cameraState.previousHeight;
  119. let newScale = this.cameraState.scale;
  120. if (widthRatio != 1 && heightRatio != 1) {
  121. newScale = this.cameraState.scale * 0.5 * (widthRatio + heightRatio);
  122. }
  123. else if (widthRatio != 1) {
  124. newScale = this.cameraState.scale * widthRatio;
  125. }
  126. else if (heightRatio != 1) {
  127. newScale = this.cameraState.scale * heightRatio;
  128. }
  129. this.body.view.scale = newScale;
  130. // this comes from the view module.
  131. var currentViewCenter = this.DOMtoCanvas({
  132. x: 0.5 * this.frame.canvas.clientWidth,
  133. y: 0.5 * this.frame.canvas.clientHeight
  134. });
  135. var distanceFromCenter = { // offset from view, distance view has to change by these x and y to center the node
  136. x: currentViewCenter.x - this.cameraState.position.x,
  137. y: currentViewCenter.y - this.cameraState.position.y
  138. };
  139. this.body.view.translation.x += distanceFromCenter.x * this.body.view.scale;
  140. this.body.view.translation.y += distanceFromCenter.y * this.body.view.scale;
  141. }
  142. }
  143. /**
  144. *
  145. * @param {number|string} value
  146. * @returns {string}
  147. * @private
  148. */
  149. _prepareValue(value) {
  150. if (typeof value === 'number') {
  151. return value + 'px';
  152. }
  153. else if (typeof value === 'string') {
  154. if (value.indexOf('%') !== -1 || value.indexOf('px') !== -1) {
  155. return value;
  156. }
  157. else if (value.indexOf('%') === -1) {
  158. return value + 'px';
  159. }
  160. }
  161. throw new Error('Could not use the value supplied for width or height:' + value);
  162. }
  163. /**
  164. * Create the HTML
  165. */
  166. _create() {
  167. // remove all elements from the container element.
  168. while (this.body.container.hasChildNodes()) {
  169. this.body.container.removeChild(this.body.container.firstChild);
  170. }
  171. this.frame = document.createElement('div');
  172. this.frame.className = 'vis-network';
  173. this.frame.style.position = 'relative';
  174. this.frame.style.overflow = 'hidden';
  175. this.frame.tabIndex = 900; // tab index is required for keycharm to bind keystrokes to the div instead of the window
  176. //////////////////////////////////////////////////////////////////
  177. this.frame.canvas = document.createElement("canvas");
  178. this.frame.canvas.style.position = 'relative';
  179. this.frame.appendChild(this.frame.canvas);
  180. if (!this.frame.canvas.getContext) {
  181. let noCanvas = document.createElement( 'DIV' );
  182. noCanvas.style.color = 'red';
  183. noCanvas.style.fontWeight = 'bold' ;
  184. noCanvas.style.padding = '10px';
  185. noCanvas.innerHTML = 'Error: your browser does not support HTML canvas';
  186. this.frame.canvas.appendChild(noCanvas);
  187. }
  188. else {
  189. this._setPixelRatio();
  190. this.setTransform();
  191. }
  192. // add the frame to the container element
  193. this.body.container.appendChild(this.frame);
  194. this.body.view.scale = 1;
  195. this.body.view.translation = {x: 0.5 * this.frame.canvas.clientWidth,y: 0.5 * this.frame.canvas.clientHeight};
  196. this._bindHammer();
  197. }
  198. /**
  199. * This function binds hammer, it can be repeated over and over due to the uniqueness check.
  200. * @private
  201. */
  202. _bindHammer() {
  203. if (this.hammer !== undefined) {
  204. this.hammer.destroy();
  205. }
  206. this.drag = {};
  207. this.pinch = {};
  208. // init hammer
  209. this.hammer = new Hammer(this.frame.canvas);
  210. this.hammer.get('pinch').set({enable: true});
  211. // enable to get better response, todo: test on mobile.
  212. this.hammer.get('pan').set({threshold:5, direction: Hammer.DIRECTION_ALL});
  213. hammerUtil.onTouch(this.hammer, (event) => {this.body.eventListeners.onTouch(event)});
  214. this.hammer.on('tap', (event) => {this.body.eventListeners.onTap(event)});
  215. this.hammer.on('doubletap', (event) => {this.body.eventListeners.onDoubleTap(event)});
  216. this.hammer.on('press', (event) => {this.body.eventListeners.onHold(event)});
  217. this.hammer.on('panstart', (event) => {this.body.eventListeners.onDragStart(event)});
  218. this.hammer.on('panmove', (event) => {this.body.eventListeners.onDrag(event)});
  219. this.hammer.on('panend', (event) => {this.body.eventListeners.onDragEnd(event)});
  220. this.hammer.on('pinch', (event) => {this.body.eventListeners.onPinch(event)});
  221. // TODO: neatly cleanup these handlers when re-creating the Canvas, IF these are done with hammer, event.stopPropagation will not work?
  222. this.frame.canvas.addEventListener('mousewheel', (event) => {this.body.eventListeners.onMouseWheel(event)});
  223. this.frame.canvas.addEventListener('DOMMouseScroll', (event) => {this.body.eventListeners.onMouseWheel(event)});
  224. this.frame.canvas.addEventListener('mousemove', (event) => {this.body.eventListeners.onMouseMove(event)});
  225. this.frame.canvas.addEventListener('contextmenu', (event) => {this.body.eventListeners.onContext(event)});
  226. this.hammerFrame = new Hammer(this.frame);
  227. hammerUtil.onRelease(this.hammerFrame, (event) => {this.body.eventListeners.onRelease(event)});
  228. }
  229. /**
  230. * Set a new size for the network
  231. * @param {string} width Width in pixels or percentage (for example '800px'
  232. * or '50%')
  233. * @param {string} height Height in pixels or percentage (for example '400px'
  234. * or '30%')
  235. * @returns {boolean}
  236. */
  237. setSize(width = this.options.width, height = this.options.height) {
  238. width = this._prepareValue(width);
  239. height= this._prepareValue(height);
  240. let emitEvent = false;
  241. let oldWidth = this.frame.canvas.width;
  242. let oldHeight = this.frame.canvas.height;
  243. // update the pixel ratio
  244. //
  245. // NOTE: Comment in following is rather inconsistent; this is the ONLY place in the code
  246. // where it is assumed that the pixel ratio could change at runtime.
  247. // The only way I can think of this happening is a rotating screen or tablet; but then
  248. // there should be a mechanism for reloading the data (TODO: check if this is present).
  249. //
  250. // If the assumption is true (i.e. pixel ratio can change at runtime), then *all* usage
  251. // of pixel ratio must be overhauled for this.
  252. //
  253. // For the time being, I will humor the assumption here, and in the rest of the code assume it is
  254. // constant.
  255. let previousRatio = this.pixelRatio; // we cache this because the camera state storage needs the old value
  256. this._setPixelRatio();
  257. if (width != this.options.width || height != this.options.height || this.frame.style.width != width || this.frame.style.height != height) {
  258. this._getCameraState(previousRatio);
  259. this.frame.style.width = width;
  260. this.frame.style.height = height;
  261. this.frame.canvas.style.width = '100%';
  262. this.frame.canvas.style.height = '100%';
  263. this.frame.canvas.width = Math.round(this.frame.canvas.clientWidth * this.pixelRatio);
  264. this.frame.canvas.height = Math.round(this.frame.canvas.clientHeight * this.pixelRatio);
  265. this.options.width = width;
  266. this.options.height = height;
  267. this.canvasViewCenter = {
  268. x: 0.5 * this.frame.clientWidth,
  269. y: 0.5 * this.frame.clientHeight
  270. };
  271. emitEvent = true;
  272. }
  273. else {
  274. // this would adapt the width of the canvas to the width from 100% if and only if
  275. // there is a change.
  276. let newWidth = Math.round(this.frame.canvas.clientWidth * this.pixelRatio);
  277. let newHeight = Math.round(this.frame.canvas.clientHeight * this.pixelRatio);
  278. // store the camera if there is a change in size.
  279. if (this.frame.canvas.width !== newWidth || this.frame.canvas.height !== newHeight) {
  280. this._getCameraState(previousRatio);
  281. }
  282. if (this.frame.canvas.width !== newWidth) {
  283. this.frame.canvas.width = newWidth;
  284. emitEvent = true;
  285. }
  286. if (this.frame.canvas.height !== newHeight) {
  287. this.frame.canvas.height = newHeight;
  288. emitEvent = true;
  289. }
  290. }
  291. if (emitEvent === true) {
  292. this.body.emitter.emit('resize', {
  293. width : Math.round(this.frame.canvas.width / this.pixelRatio),
  294. height : Math.round(this.frame.canvas.height / this.pixelRatio),
  295. oldWidth : Math.round(oldWidth / this.pixelRatio),
  296. oldHeight: Math.round(oldHeight / this.pixelRatio)
  297. });
  298. // restore the camera on change.
  299. this._setCameraState();
  300. }
  301. // set initialized so the get and set camera will work from now on.
  302. this.initialized = true;
  303. return emitEvent;
  304. }
  305. /**
  306. *
  307. * @returns {CanvasRenderingContext2D}
  308. */
  309. getContext() {
  310. return this.frame.canvas.getContext("2d");
  311. }
  312. /**
  313. * Determine the pixel ratio for various browsers.
  314. *
  315. * @returns {number}
  316. * @private
  317. */
  318. _determinePixelRatio() {
  319. let ctx = this.getContext();
  320. if (ctx === undefined) {
  321. throw new Error("Could not get canvax context");
  322. }
  323. return (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio ||
  324. ctx.mozBackingStorePixelRatio ||
  325. ctx.msBackingStorePixelRatio ||
  326. ctx.oBackingStorePixelRatio ||
  327. ctx.backingStorePixelRatio || 1);
  328. }
  329. /**
  330. * Lazy determination of pixel ratio.
  331. *
  332. * @private
  333. */
  334. _setPixelRatio() {
  335. this.pixelRatio = this._determinePixelRatio();
  336. }
  337. /**
  338. * Set the transform in the contained context, based on its pixelRatio
  339. */
  340. setTransform() {
  341. let ctx = this.getContext();
  342. if (ctx === undefined) {
  343. throw new Error("Could not get canvax context");
  344. }
  345. ctx.setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0);
  346. }
  347. /**
  348. * Convert the X coordinate in DOM-space (coordinate point in browser relative to the container div) to
  349. * the X coordinate in canvas-space (the simulation sandbox, which the camera looks upon)
  350. * @param {number} x
  351. * @returns {number}
  352. * @private
  353. */
  354. _XconvertDOMtoCanvas(x) {
  355. return (x - this.body.view.translation.x) / this.body.view.scale;
  356. }
  357. /**
  358. * Convert the X coordinate in canvas-space (the simulation sandbox, which the camera looks upon) to
  359. * the X coordinate in DOM-space (coordinate point in browser relative to the container div)
  360. * @param {number} x
  361. * @returns {number}
  362. * @private
  363. */
  364. _XconvertCanvasToDOM(x) {
  365. return x * this.body.view.scale + this.body.view.translation.x;
  366. }
  367. /**
  368. * Convert the Y coordinate in DOM-space (coordinate point in browser relative to the container div) to
  369. * the Y coordinate in canvas-space (the simulation sandbox, which the camera looks upon)
  370. * @param {number} y
  371. * @returns {number}
  372. * @private
  373. */
  374. _YconvertDOMtoCanvas(y) {
  375. return (y - this.body.view.translation.y) / this.body.view.scale;
  376. }
  377. /**
  378. * Convert the Y coordinate in canvas-space (the simulation sandbox, which the camera looks upon) to
  379. * the Y coordinate in DOM-space (coordinate point in browser relative to the container div)
  380. * @param {number} y
  381. * @returns {number}
  382. * @private
  383. */
  384. _YconvertCanvasToDOM(y) {
  385. return y * this.body.view.scale + this.body.view.translation.y;
  386. }
  387. /**
  388. *
  389. * @param {object} pos = {x: number, y: number}
  390. * @returns {{x: number, y: number}}
  391. * @constructor
  392. */
  393. canvasToDOM (pos) {
  394. return {x: this._XconvertCanvasToDOM(pos.x), y: this._YconvertCanvasToDOM(pos.y)};
  395. }
  396. /**
  397. *
  398. * @param {object} pos = {x: number, y: number}
  399. * @returns {{x: number, y: number}}
  400. * @constructor
  401. */
  402. DOMtoCanvas (pos) {
  403. return {x: this._XconvertDOMtoCanvas(pos.x), y: this._YconvertDOMtoCanvas(pos.y)};
  404. }
  405. }
  406. export default Canvas;