cytoscape
Version:
Graph theory (a.k.a. network) library for analysis and visualisation
661 lines (489 loc) • 17.1 kB
JavaScript
import * as util from '../../../util';
import * as math from '../../../math';
import Heap from '../../../heap';
import * as is from '../../../is';
import defs from './texture-cache-defs';
var defNumLayers = 1; // default number of layers to use
var minLvl = -4; // when scaling smaller than that we don't need to re-render
var maxLvl = 2; // when larger than this scale just render directly (caching is not helpful)
var maxZoom = 3.99; // beyond this zoom level, layered textures are not used
var deqRedrawThreshold = 50; // time to batch redraws together from dequeueing to allow more dequeueing calcs to happen in the meanwhile
var refineEleDebounceTime = 50; // time to debounce sharper ele texture updates
var disableEleImgSmoothing = true; // when drawing eles on layers from an ele cache ; crisper and more performant when true
var deqCost = 0.15; // % of add'l rendering cost allowed for dequeuing ele caches each frame
var deqAvgCost = 0.1; // % of add'l rendering cost compared to average overall redraw time
var deqNoDrawCost = 0.9; // % of avg frame time that can be used for dequeueing when not drawing
var deqFastCost = 0.9; // % of frame time to be used when >60fps
var maxDeqSize = 1; // number of eles to dequeue and render at higher texture in each batch
var invalidThreshold = 250; // time threshold for disabling b/c of invalidations
var maxLayerArea = 4000 * 4000; // layers can't be bigger than this
var alwaysQueue = true; // never draw all the layers in a level on a frame; draw directly until all dequeued
var useHighQualityEleTxrReqs = true; // whether to use high quality ele txr requests (generally faster and cheaper in the longterm)
var useEleTxrCaching = true; // whether to use individual ele texture caching underneath this cache
// var log = function(){ console.log.apply( console, arguments ); };
var LayeredTextureCache = function( renderer ){
var self = this;
var r = self.renderer = renderer;
var cy = r.cy;
self.layersByLevel = {}; // e.g. 2 => [ layer1, layer2, ..., layerN ]
self.firstGet = true;
self.lastInvalidationTime = util.performanceNow() - 2*invalidThreshold;
self.skipping = false;
self.eleTxrDeqs = cy.collection();
self.scheduleElementRefinement = util.debounce( function(){
self.refineElementTextures( self.eleTxrDeqs );
self.eleTxrDeqs.unmerge( self.eleTxrDeqs );
}, refineEleDebounceTime );
r.beforeRender(function( willDraw, now ){
if( now - self.lastInvalidationTime <= invalidThreshold ){
self.skipping = true;
} else {
self.skipping = false;
}
}, r.beforeRenderPriorities.lyrTxrSkip);
var qSort = function(a, b){
return b.reqs - a.reqs;
};
self.layersQueue = new Heap( qSort );
self.setupDequeueing();
};
var LTCp = LayeredTextureCache.prototype;
var layerIdPool = 0;
var MAX_INT = Math.pow(2, 53) - 1;
LTCp.makeLayer = function( bb, lvl ){
var scale = Math.pow( 2, lvl );
var w = Math.ceil( bb.w * scale );
var h = Math.ceil( bb.h * scale );
var canvas = this.renderer.makeOffscreenCanvas(w, h);
var layer = {
id: (layerIdPool = ++layerIdPool % MAX_INT ),
bb: bb,
level: lvl,
width: w,
height: h,
canvas: canvas,
context: canvas.getContext('2d'),
eles: [],
elesQueue: [],
reqs: 0
};
// log('make layer %s with w %s and h %s and lvl %s', layer.id, layer.width, layer.height, layer.level);
var cxt = layer.context;
var dx = -layer.bb.x1;
var dy = -layer.bb.y1;
// do the transform on creation to save cycles (it's the same for all eles)
cxt.scale( scale, scale );
cxt.translate( dx, dy );
return layer;
};
LTCp.getLayers = function( eles, pxRatio, lvl ){
var self = this;
var r = self.renderer;
var cy = r.cy;
var zoom = cy.zoom();
var firstGet = self.firstGet;
self.firstGet = false;
// log('--\nget layers with %s eles', eles.length);
//log eles.map(function(ele){ return ele.id() }) );
if( lvl == null ){
lvl = Math.ceil( math.log2( zoom * pxRatio ) );
if( lvl < minLvl ){
lvl = minLvl;
} else if( zoom >= maxZoom || lvl > maxLvl ){
return null;
}
}
self.validateLayersElesOrdering( lvl, eles );
var layersByLvl = self.layersByLevel;
var scale = Math.pow( 2, lvl );
var layers = layersByLvl[ lvl ] = layersByLvl[ lvl ] || [];
var bb;
var lvlComplete = self.levelIsComplete( lvl, eles );
var tmpLayers;
var checkTempLevels = function(){
var canUseAsTmpLvl = function( l ){
self.validateLayersElesOrdering( l, eles );
if( self.levelIsComplete( l, eles ) ){
tmpLayers = layersByLvl[l];
return true;
}
};
var checkLvls = function( dir ){
if( tmpLayers ){ return; }
for( var l = lvl + dir; minLvl <= l && l <= maxLvl; l += dir ){
if( canUseAsTmpLvl(l) ){ break; }
}
};
checkLvls( +1 );
checkLvls( -1 );
// remove the invalid layers; they will be replaced as needed later in this function
for( var i = layers.length - 1; i >= 0; i-- ){
var layer = layers[i];
if( layer.invalid ){
util.removeFromArray( layers, layer );
}
}
};
if( !lvlComplete ){
// if the current level is incomplete, then use the closest, best quality layerset temporarily
// and later queue the current layerset so we can get the proper quality level soon
checkTempLevels();
} else {
// log('level complete, using existing layers\n--');
return layers;
}
var getBb = function(){
if( !bb ){
bb = math.makeBoundingBox();
for( var i = 0; i < eles.length; i++ ){
math.updateBoundingBox( bb, eles[i].boundingBox() );
}
}
return bb;
};
var makeLayer = function( opts ){
opts = opts || {};
var after = opts.after;
getBb();
var area = ( bb.w * scale ) * ( bb.h * scale );
if( area > maxLayerArea ){
return null;
}
var layer = self.makeLayer( bb, lvl );
if( after != null ){
var index = layers.indexOf( after ) + 1;
layers.splice( index, 0, layer );
} else if( opts.insert === undefined || opts.insert ){
// no after specified => first layer made so put at start
layers.unshift( layer );
}
// if( tmpLayers ){
//self.queueLayer( layer );
// }
return layer;
};
if( self.skipping && !firstGet ){
// log('skip layers');
return null;
}
// log('do layers');
var layer = null;
var maxElesPerLayer = eles.length / defNumLayers;
var allowLazyQueueing = alwaysQueue && !firstGet;
for( var i = 0; i < eles.length; i++ ){
var ele = eles[i];
var rs = ele._private.rscratch;
var caches = rs.imgLayerCaches = rs.imgLayerCaches || {};
// log('look at ele', ele.id());
var existingLayer = caches[ lvl ];
if( existingLayer ){
// reuse layer for later eles
// log('reuse layer for', ele.id());
layer = existingLayer;
continue;
}
if(
!layer
|| layer.eles.length >= maxElesPerLayer
|| !math.boundingBoxInBoundingBox( layer.bb, ele.boundingBox() )
){
// log('make new layer for ele %s', ele.id());
layer = makeLayer({ insert: true, after: layer });
// if now layer can be built then we can't use layers at this level
if( !layer ){ return null; }
// log('new layer with id %s', layer.id);
}
if( tmpLayers || allowLazyQueueing ){
// log('queue ele %s in layer %s', ele.id(), layer.id);
self.queueLayer( layer, ele );
} else {
// log('draw ele %s in layer %s', ele.id(), layer.id);
self.drawEleInLayer( layer, ele, lvl, pxRatio );
}
layer.eles.push( ele );
caches[ lvl ] = layer;
}
// log('--');
if( tmpLayers ){ // then we only queued the current layerset and can't draw it yet
return tmpLayers;
}
if( allowLazyQueueing ){
// log('lazy queue level', lvl);
return null;
}
return layers;
};
// a layer may want to use an ele cache of a higher level to avoid blurriness
// so the layer level might not equal the ele level
LTCp.getEleLevelForLayerLevel = function( lvl, pxRatio ){
return lvl;
};
LTCp.drawEleInLayer = function( layer, ele, lvl, pxRatio ){
var self = this;
var r = this.renderer;
var context = layer.context;
var bb = ele.boundingBox();
if( bb.w === 0 || bb.h === 0 || !ele.visible() ){ return; }
lvl = self.getEleLevelForLayerLevel( lvl, pxRatio );
if( disableEleImgSmoothing ){ r.setImgSmoothing( context, false ); }
if( useEleTxrCaching ){
r.drawCachedElement( context, ele, null, null, lvl, useHighQualityEleTxrReqs );
} else { // if the element is not cacheable, then draw directly
r.drawElement( context, ele );
}
if( disableEleImgSmoothing ){ r.setImgSmoothing( context, true ); }
};
LTCp.levelIsComplete = function( lvl, eles ){
var self = this;
var layers = self.layersByLevel[ lvl ];
if( !layers || layers.length === 0 ){ return false; }
var numElesInLayers = 0;
for( var i = 0; i < layers.length; i++ ){
var layer = layers[i];
// if there are any eles needed to be drawn yet, the level is not complete
if( layer.reqs > 0 ){ return false; }
// if the layer is invalid, the level is not complete
if( layer.invalid ){ return false; }
numElesInLayers += layer.eles.length;
}
// we should have exactly the number of eles passed in to be complete
if( numElesInLayers !== eles.length ){ return false; }
return true;
};
LTCp.validateLayersElesOrdering = function( lvl, eles ){
var layers = this.layersByLevel[ lvl ];
if( !layers ){ return; }
// if in a layer the eles are not in the same order, then the layer is invalid
// (i.e. there is an ele in between the eles in the layer)
for( var i = 0; i < layers.length; i++ ){
var layer = layers[i];
var offset = -1;
// find the offset
for( var j = 0; j < eles.length; j++ ){
if( layer.eles[0] === eles[j] ){
offset = j;
break;
}
}
if( offset < 0 ){
// then the layer has nonexistant elements and is invalid
this.invalidateLayer( layer );
continue;
}
// the eles in the layer must be in the same continuous order, else the layer is invalid
var o = offset;
for( var j = 0; j < layer.eles.length; j++ ){
if( layer.eles[j] !== eles[o+j] ){
// log('invalidate based on ordering', layer.id);
this.invalidateLayer( layer );
break;
}
}
}
};
LTCp.updateElementsInLayers = function( eles, update ){
var self = this;
var isEles = is.element( eles[0] );
// collect udpated elements (cascaded from the layers) and update each
// layer itself along the way
for( var i = 0; i < eles.length; i++ ){
var req = isEles ? null : eles[i];
var ele = isEles ? eles[i] : eles[i].ele;
var rs = ele._private.rscratch;
var caches = rs.imgLayerCaches = rs.imgLayerCaches || {};
for( var l = minLvl; l <= maxLvl; l++ ){
var layer = caches[l];
if( !layer ){ continue; }
// if update is a request from the ele cache, then it affects only
// the matching level
if( req && self.getEleLevelForLayerLevel( layer.level ) !== req.level ){
continue;
}
update( layer, ele, req );
}
}
};
LTCp.haveLayers = function(){
var self = this;
var haveLayers = false;
for( var l = minLvl; l <= maxLvl; l++ ){
var layers = self.layersByLevel[l];
if( layers && layers.length > 0 ){
haveLayers = true;
break;
}
}
return haveLayers;
};
LTCp.invalidateElements = function( eles ){
var self = this;
if( eles.length === 0 ){ return; }
self.lastInvalidationTime = util.performanceNow();
// log('update invalidate layer time from eles');
if( eles.length === 0 || !self.haveLayers() ){ return; }
self.updateElementsInLayers( eles, function invalAssocLayers( layer, ele, req ){
self.invalidateLayer( layer );
} );
};
LTCp.invalidateLayer = function( layer ){
// log('update invalidate layer time');
this.lastInvalidationTime = util.performanceNow();
if( layer.invalid ){ return; } // save cycles
var lvl = layer.level;
var eles = layer.eles;
var layers = this.layersByLevel[ lvl ];
// log('invalidate layer', layer.id );
util.removeFromArray( layers, layer );
// layer.eles = [];
layer.elesQueue = [];
layer.invalid = true;
if( layer.replacement ){
layer.replacement.invalid = true;
}
for( var i = 0; i < eles.length; i++ ){
var caches = eles[i]._private.rscratch.imgLayerCaches;
if( caches ){
caches[ lvl ] = null;
}
}
};
LTCp.refineElementTextures = function( eles ){
var self = this;
// log('refine', eles.length);
self.updateElementsInLayers( eles, function refineEachEle( layer, ele, req ){
var rLyr = layer.replacement;
if( !rLyr ){
rLyr = layer.replacement = self.makeLayer( layer.bb, layer.level );
rLyr.replaces = layer;
rLyr.eles = layer.eles;
// log('make replacement layer %s for %s with level %s', rLyr.id, layer.id, rLyr.level);
}
if( !rLyr.reqs ){
for( var i = 0; i < rLyr.eles.length; i++ ){
self.queueLayer( rLyr, rLyr.eles[i] );
}
// log('queue replacement layer refinement', rLyr.id);
}
} );
};
LTCp.enqueueElementRefinement = function( ele ){
if( !useEleTxrCaching ){ return; }
this.eleTxrDeqs.merge( ele );
this.scheduleElementRefinement();
};
LTCp.queueLayer = function( layer, ele ){
var self = this;
var q = self.layersQueue;
var elesQ = layer.elesQueue;
var hasId = elesQ.hasId = elesQ.hasId || {};
// if a layer is going to be replaced, queuing is a waste of time
if( layer.replacement ){ return; }
if( ele ){
if( hasId[ ele.id() ] ){
return;
}
elesQ.push( ele );
hasId[ ele.id() ] = true;
}
if( layer.reqs ){
layer.reqs++;
q.updateItem( layer );
} else {
layer.reqs = 1;
q.push( layer );
}
};
LTCp.dequeue = function( pxRatio ){
var self = this;
var q = self.layersQueue;
var deqd = [];
var eleDeqs = 0;
while( eleDeqs < maxDeqSize ){
if( q.size() === 0 ){ break; }
var layer = q.peek();
// if a layer has been or will be replaced, then don't waste time with it
if( layer.replacement ){
// log('layer %s in queue skipped b/c it already has a replacement', layer.id);
q.pop();
continue;
}
// if this is a replacement layer that has been superceded, then forget it
if( layer.replaces && layer !== layer.replaces.replacement ){
// log('layer is no longer the most uptodate replacement; dequeued', layer.id)
q.pop();
continue;
}
if( layer.invalid ){
// log('replacement layer %s is invalid; dequeued', layer.id);
q.pop();
continue;
}
var ele = layer.elesQueue.shift();
if( ele ){
// log('dequeue layer %s', layer.id);
self.drawEleInLayer( layer, ele, layer.level, pxRatio );
eleDeqs++;
}
if( deqd.length === 0 ){
// we need only one entry in deqd to queue redrawing etc
deqd.push( true );
}
// if the layer has all its eles done, then remove from the queue
if( layer.elesQueue.length === 0 ){
q.pop();
layer.reqs = 0;
// log('dequeue of layer %s complete', layer.id);
// when a replacement layer is dequeued, it replaces the old layer in the level
if( layer.replaces ){
self.applyLayerReplacement( layer );
}
self.requestRedraw();
}
}
return deqd;
};
LTCp.applyLayerReplacement = function( layer ){
var self = this;
var layersInLevel = self.layersByLevel[ layer.level ];
var replaced = layer.replaces;
var index = layersInLevel.indexOf( replaced );
// if the replaced layer is not in the active list for the level, then replacing
// refs would be a mistake (i.e. overwriting the true active layer)
if( index < 0 || replaced.invalid ){
// log('replacement layer would have no effect', layer.id);
return;
}
layersInLevel[ index ] = layer; // replace level ref
// replace refs in eles
for( var i = 0; i < layer.eles.length; i++ ){
var _p = layer.eles[i]._private;
var cache = _p.imgLayerCaches = _p.imgLayerCaches || {};
if( cache ){
cache[ layer.level ] = layer;
}
}
// log('apply replacement layer %s over %s', layer.id, replaced.id);
self.requestRedraw();
};
LTCp.requestRedraw = util.debounce( function(){
var r = this.renderer;
r.redrawHint( 'eles', true );
r.redrawHint( 'drag', true );
r.redraw();
}, 100 );
LTCp.setupDequeueing = defs.setupDequeueing({
deqRedrawThreshold: deqRedrawThreshold,
deqCost: deqCost,
deqAvgCost: deqAvgCost,
deqNoDrawCost: deqNoDrawCost,
deqFastCost: deqFastCost,
deq: function( self, pxRatio ){
return self.dequeue( pxRatio );
},
onDeqd: util.noop,
shouldRedraw: util.trueify,
priority: function( self ){
return self.renderer.beforeRenderPriorities.lyrTxrDeq;
}
});
export default LayeredTextureCache;