/**
|
|
* 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;
|