/** * Initializes window.requestAnimationFrame() to a usable form. * * Specifically, set up this method for the case of running on node.js with jsdom enabled. * * NOTES: * * * On node.js, when calling this directly outside of this class, `window` is not defined. * This happens even if jsdom is used. * * For node.js + jsdom, `window` is available at the moment the constructor is called. * For this reason, the called is placed within the constructor. * * Even then, `window.requestAnimationFrame()` is not defined, so it still needs to be added. * * During unit testing, it happens that the window object is reset during execution, causing * a runtime error due to missing `requestAnimationFrame()`. This needs to be compensated for, * see `_requestNextFrame()`. * * Since this is a global object, it may affect other modules besides `Network`. With normal * usage, this does not cause any problems. During unit testing, errors may occur. These have * been compensated for, see comment block in _requestNextFrame(). * * @private */ function _initRequestAnimationFrame() { var func; if (window !== undefined) { func = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame; } if (func === undefined) { // window or method not present, setting mock requestAnimationFrame window.requestAnimationFrame = function(callback) { //console.log("Called mock requestAnimationFrame"); callback(); } } else { window.requestAnimationFrame = func; } } let util = require('../../util'); /** * The canvas renderer */ class CanvasRenderer { /** * @param {Object} body * @param {Canvas} canvas */ constructor(body, canvas) { _initRequestAnimationFrame(); this.body = body; this.canvas = canvas; this.redrawRequested = false; this.renderTimer = undefined; this.requiresTimeout = true; this.renderingActive = false; this.renderRequests = 0; this.allowRedraw = true; this.dragging = false; this.options = {}; this.defaultOptions = { hideEdgesOnDrag: false, hideNodesOnDrag: false }; util.extend(this.options, this.defaultOptions); this._determineBrowserMethod(); this.bindEventListeners(); } /** * Binds event listeners */ bindEventListeners() { this.body.emitter.on("dragStart", () => { this.dragging = true; }); this.body.emitter.on("dragEnd", () => { this.dragging = false; }); this.body.emitter.on("_resizeNodes", () => { this._resizeNodes(); }); this.body.emitter.on("_redraw", () => { if (this.renderingActive === false) { this._redraw(); } }); this.body.emitter.on("_blockRedraw", () => {this.allowRedraw = false;}); this.body.emitter.on("_allowRedraw", () => {this.allowRedraw = true; this.redrawRequested = false;}); this.body.emitter.on("_requestRedraw", this._requestRedraw.bind(this)); this.body.emitter.on("_startRendering", () => { this.renderRequests += 1; this.renderingActive = true; this._startRendering(); }); this.body.emitter.on("_stopRendering", () => { this.renderRequests -= 1; this.renderingActive = this.renderRequests > 0; this.renderTimer = undefined; }); this.body.emitter.on('destroy', () => { this.renderRequests = 0; this.allowRedraw = false; this.renderingActive = false; if (this.requiresTimeout === true) { clearTimeout(this.renderTimer); } else { window.cancelAnimationFrame(this.renderTimer); } this.body.emitter.off(); }); } /** * * @param {Object} options */ setOptions(options) { if (options !== undefined) { let fields = ['hideEdgesOnDrag','hideNodesOnDrag']; util.selectiveDeepExtend(fields,this.options, options); } } /** * Prepare the drawing of the next frame. * * Calls the callback when the next frame can or will be drawn. * * @param {function} callback * @param {number} delay - timeout case only, wait this number of milliseconds * @returns {function|undefined} * @private */ _requestNextFrame(callback, delay) { // During unit testing, it happens that the mock window object is reset while // the next frame is still pending. Then, either 'window' is not present, or // 'requestAnimationFrame()' is not present because it is not defined on the // mock window object. // // As a consequence, unrelated unit tests may appear to fail, even if the problem // described happens in the current unit test. // // This is not something that will happen in normal operation, but we still need // to take it into account. // var myWindow = window; // Grab a reference to reduce the possibility that 'window' is reset // while running this method. if (myWindow === undefined) return; let timer; if (this.requiresTimeout === true) { // wait given number of milliseconds and perform the animation step function timer = myWindow.setTimeout(callback, delay); } else { if (myWindow.requestAnimationFrame) { timer = myWindow.requestAnimationFrame(callback); } } return timer; } /** * * @private */ _startRendering() { if (this.renderingActive === true) { if (this.renderTimer === undefined) { this.renderTimer = this._requestNextFrame(this._renderStep.bind(this), this.simulationInterval); } } } /** * * @private */ _renderStep() { if (this.renderingActive === true) { // reset the renderTimer so a new scheduled animation step can be set this.renderTimer = undefined; if (this.requiresTimeout === true) { // this schedules a new simulation step this._startRendering(); } this._redraw(); if (this.requiresTimeout === false) { // this schedules a new simulation step this._startRendering(); } } } /** * Redraw the network with the current data * chart will be resized too. */ redraw() { this.body.emitter.emit('setSize'); this._redraw(); } /** * Redraw the network with the current data * @private */ _requestRedraw() { if (this.redrawRequested !== true && this.renderingActive === false && this.allowRedraw === true) { this.redrawRequested = true; this._requestNextFrame(() => {this._redraw(false);}, 0); } } /** * Redraw the network with the current data * @param {boolean} [hidden=false] | Used to get the first estimate of the node sizes. * Only the nodes are drawn after which they are quickly drawn over. * @private */ _redraw(hidden = false) { if (this.allowRedraw === true) { this.body.emitter.emit("initRedraw"); this.redrawRequested = false; // when the container div was hidden, this fixes it back up! if (this.canvas.frame.canvas.width === 0 || this.canvas.frame.canvas.height === 0) { this.canvas.setSize(); } this.canvas.setTransform(); let ctx = this.canvas.getContext(); // clear the canvas let w = this.canvas.frame.canvas.clientWidth; let h = this.canvas.frame.canvas.clientHeight; ctx.clearRect(0, 0, w, h); // if the div is hidden, we stop the redraw here for performance. if (this.canvas.frame.clientWidth === 0) { return; } // set scaling and translation ctx.save(); ctx.translate(this.body.view.translation.x, this.body.view.translation.y); ctx.scale(this.body.view.scale, this.body.view.scale); ctx.beginPath(); this.body.emitter.emit("beforeDrawing", ctx); ctx.closePath(); if (hidden === false) { if (this.dragging === false || (this.dragging === true && this.options.hideEdgesOnDrag === false)) { this._drawEdges(ctx); } } if (this.dragging === false || (this.dragging === true && this.options.hideNodesOnDrag === false)) { this._drawNodes(ctx, hidden); } ctx.beginPath(); this.body.emitter.emit("afterDrawing", ctx); ctx.closePath(); // restore original scaling and translation ctx.restore(); if (hidden === true) { ctx.clearRect(0, 0, w, h); } } } /** * Redraw all nodes * * @param {CanvasRenderingContext2D} ctx * @param {boolean} [alwaysShow] * @private */ _resizeNodes() { this.canvas.setTransform(); let ctx = this.canvas.getContext(); ctx.save(); ctx.translate(this.body.view.translation.x, this.body.view.translation.y); ctx.scale(this.body.view.scale, this.body.view.scale); let nodes = this.body.nodes; let node; // resize all nodes for (let nodeId in nodes) { if (nodes.hasOwnProperty(nodeId)) { node = nodes[nodeId]; node.resize(ctx); node.updateBoundingBox(ctx, node.selected); } } // restore original scaling and translation ctx.restore(); } /** * Redraw all nodes * * @param {CanvasRenderingContext2D} ctx 2D context of a HTML canvas * @param {boolean} [alwaysShow] * @private */ _drawNodes(ctx, alwaysShow = false) { let nodes = this.body.nodes; let nodeIndices = this.body.nodeIndices; let node; let selected = []; let margin = 20; let topLeft = this.canvas.DOMtoCanvas({x:-margin,y:-margin}); let bottomRight = this.canvas.DOMtoCanvas({ x: this.canvas.frame.canvas.clientWidth+margin, y: this.canvas.frame.canvas.clientHeight+margin }); let viewableArea = {top:topLeft.y,left:topLeft.x,bottom:bottomRight.y,right:bottomRight.x}; // draw unselected nodes; for (let i = 0; i < nodeIndices.length; i++) { node = nodes[nodeIndices[i]]; // set selected nodes aside if (node.isSelected()) { selected.push(nodeIndices[i]); } else { if (alwaysShow === true) { node.draw(ctx); } else if (node.isBoundingBoxOverlappingWith(viewableArea) === true) { node.draw(ctx); } else { node.updateBoundingBox(ctx, node.selected); } } } // draw the selected nodes on top for (let i = 0; i < selected.length; i++) { node = nodes[selected[i]]; node.draw(ctx); } } /** * Redraw all edges * @param {CanvasRenderingContext2D} ctx 2D context of a HTML canvas * @private */ _drawEdges(ctx) { let edges = this.body.edges; let edgeIndices = this.body.edgeIndices; let edge; for (let i = 0; i < edgeIndices.length; i++) { edge = edges[edgeIndices[i]]; if (edge.connected === true) { edge.draw(ctx); } } } /** * Determine if the browser requires a setTimeout or a requestAnimationFrame. This was required because * some implementations (safari and IE9) did not support requestAnimationFrame * @private */ _determineBrowserMethod() { if (typeof window !== 'undefined') { let browserType = navigator.userAgent.toLowerCase(); this.requiresTimeout = false; if (browserType.indexOf('msie 9.0') != -1) { // IE 9 this.requiresTimeout = true; } else if (browserType.indexOf('safari') != -1) { // safari if (browserType.indexOf('chrome') <= -1) { this.requiresTimeout = true; } } } else { this.requiresTimeout = true; } } } export default CanvasRenderer;