|
import * as math from '../../../math'; |
|
import { trueify, falsify, removeFromArray, clearArray, MAX_INT, assign, defaults } from '../../../util'; |
|
import Heap from '../../../heap'; |
|
import defs from './texture-cache-defs'; |
|
import ElementTextureCacheLookup from './ele-texture-cache-lookup'; |
|
|
|
const minTxrH = 25; |
|
const txrStepH = 50; |
|
const minLvl = -4; |
|
const maxLvl = 3; |
|
const maxZoom = 7.99; |
|
const eleTxrSpacing = 8; |
|
const defTxrWidth = 1024; |
|
const maxTxrW = 1024; |
|
const maxTxrH = 1024; |
|
const minUtility = 0.2; |
|
const maxFullness = 0.8; |
|
const maxFullnessChecks = 10; |
|
const deqCost = 0.15; |
|
const deqAvgCost = 0.1; |
|
const deqNoDrawCost = 0.9; |
|
const deqFastCost = 0.9; |
|
const deqRedrawThreshold = 100; |
|
const maxDeqSize = 1; |
|
|
|
const getTxrReasons = { |
|
dequeue: 'dequeue', |
|
downscale: 'downscale', |
|
highQuality: 'highQuality' |
|
}; |
|
|
|
const initDefaults = defaults({ |
|
getKey: null, |
|
doesEleInvalidateKey: falsify, |
|
drawElement: null, |
|
getBoundingBox: null, |
|
getRotationPoint: null, |
|
getRotationOffset: null, |
|
isVisible: trueify, |
|
allowEdgeTxrCaching: true, |
|
allowParentTxrCaching: true |
|
}); |
|
|
|
const ElementTextureCache = function( renderer, initOptions ){ |
|
let self = this; |
|
|
|
self.renderer = renderer; |
|
self.onDequeues = []; |
|
|
|
let opts = initDefaults(initOptions); |
|
|
|
assign(self, opts); |
|
|
|
self.lookup = new ElementTextureCacheLookup(opts.getKey, opts.doesEleInvalidateKey); |
|
|
|
self.setupDequeueing(); |
|
}; |
|
|
|
const ETCp = ElementTextureCache.prototype; |
|
|
|
ETCp.reasons = getTxrReasons; |
|
|
|
|
|
ETCp.getTextureQueue = function( txrH ){ |
|
let self = this; |
|
self.eleImgCaches = self.eleImgCaches || {}; |
|
|
|
return ( self.eleImgCaches[ txrH ] = self.eleImgCaches[ txrH ] || [] ); |
|
}; |
|
|
|
|
|
ETCp.getRetiredTextureQueue = function( txrH ){ |
|
let self = this; |
|
|
|
let rtxtrQs = self.eleImgCaches.retired = self.eleImgCaches.retired || {}; |
|
let rtxtrQ = rtxtrQs[ txrH ] = rtxtrQs[ txrH ] || []; |
|
|
|
return rtxtrQ; |
|
}; |
|
|
|
|
|
ETCp.getElementQueue = function(){ |
|
let self = this; |
|
|
|
let q = self.eleCacheQueue = self.eleCacheQueue || new Heap(function( a, b ){ |
|
return b.reqs - a.reqs; |
|
}); |
|
|
|
return q; |
|
}; |
|
|
|
|
|
ETCp.getElementKeyToQueue = function(){ |
|
let self = this; |
|
|
|
let k2q = self.eleKeyToCacheQueue = self.eleKeyToCacheQueue || {}; |
|
|
|
return k2q; |
|
}; |
|
|
|
ETCp.getElement = function( ele, bb, pxRatio, lvl, reason ){ |
|
let self = this; |
|
let r = this.renderer; |
|
let zoom = r.cy.zoom(); |
|
let lookup = this.lookup; |
|
|
|
if( !bb || bb.w === 0 || bb.h === 0 || isNaN(bb.w) || isNaN(bb.h) || !ele.visible() || ele.removed() ){ return null; } |
|
|
|
if( |
|
( !self.allowEdgeTxrCaching && ele.isEdge() ) |
|
|| ( !self.allowParentTxrCaching && ele.isParent() ) |
|
){ |
|
return null; |
|
} |
|
|
|
if( lvl == null ){ |
|
lvl = Math.ceil( math.log2( zoom * pxRatio ) ); |
|
} |
|
|
|
if( lvl < minLvl ){ |
|
lvl = minLvl; |
|
} else if( zoom >= maxZoom || lvl > maxLvl ){ |
|
return null; |
|
} |
|
|
|
let scale = Math.pow( 2, lvl ); |
|
let eleScaledH = bb.h * scale; |
|
let eleScaledW = bb.w * scale; |
|
let scaledLabelShown = r.eleTextBiggerThanMin( ele, scale ); |
|
|
|
if( !this.isVisible(ele, scaledLabelShown) ){ return null; } |
|
|
|
let eleCache = lookup.get( ele, lvl ); |
|
|
|
|
|
if( eleCache && eleCache.invalidated ){ |
|
eleCache.invalidated = false; |
|
eleCache.texture.invalidatedWidth -= eleCache.width; |
|
} |
|
|
|
if( eleCache ){ |
|
return eleCache; |
|
} |
|
|
|
let txrH; |
|
|
|
if( eleScaledH <= minTxrH ){ |
|
txrH = minTxrH; |
|
} else if( eleScaledH <= txrStepH ){ |
|
txrH = txrStepH; |
|
} else { |
|
txrH = Math.ceil( eleScaledH / txrStepH ) * txrStepH; |
|
} |
|
|
|
if( eleScaledH > maxTxrH || eleScaledW > maxTxrW ){ |
|
return null; |
|
} |
|
|
|
let txrQ = self.getTextureQueue( txrH ); |
|
|
|
|
|
let txr = txrQ[ txrQ.length - 2 ]; |
|
|
|
let addNewTxr = function(){ |
|
return self.recycleTexture( txrH, eleScaledW ) || self.addTexture( txrH, eleScaledW ); |
|
}; |
|
|
|
|
|
if( !txr ){ |
|
txr = txrQ[ txrQ.length - 1 ]; |
|
} |
|
|
|
|
|
if( !txr ){ |
|
txr = addNewTxr(); |
|
} |
|
|
|
|
|
if( txr.width - txr.usedWidth < eleScaledW ){ |
|
txr = addNewTxr(); |
|
} |
|
|
|
let scalableFrom = function( otherCache ){ |
|
return otherCache && otherCache.scaledLabelShown === scaledLabelShown; |
|
}; |
|
|
|
let deqing = reason && reason === getTxrReasons.dequeue; |
|
let highQualityReq = reason && reason === getTxrReasons.highQuality; |
|
let downscaleReq = reason && reason === getTxrReasons.downscale; |
|
|
|
let higherCache; |
|
for( let l = lvl + 1; l <= maxLvl; l++ ){ |
|
let c = lookup.get( ele, l ); |
|
|
|
if( c ){ higherCache = c; break; } |
|
} |
|
|
|
let oneUpCache = higherCache && higherCache.level === lvl + 1 ? higherCache : null; |
|
|
|
let downscale = function(){ |
|
txr.context.drawImage( |
|
oneUpCache.texture.canvas, |
|
oneUpCache.x, 0, |
|
oneUpCache.width, oneUpCache.height, |
|
txr.usedWidth, 0, |
|
eleScaledW, eleScaledH |
|
); |
|
}; |
|
|
|
|
|
txr.context.setTransform( 1, 0, 0, 1, 0, 0 ); |
|
txr.context.clearRect( txr.usedWidth, 0, eleScaledW, txrH ); |
|
|
|
if( scalableFrom(oneUpCache) ){ |
|
|
|
downscale(); |
|
|
|
} else if( scalableFrom(higherCache) ){ |
|
|
|
|
|
|
|
if( highQualityReq ){ |
|
for( let l = higherCache.level; l > lvl; l-- ){ |
|
oneUpCache = self.getElement( ele, bb, pxRatio, l, getTxrReasons.downscale ); |
|
} |
|
|
|
downscale(); |
|
|
|
} else { |
|
self.queueElement( ele, higherCache.level - 1 ); |
|
|
|
return higherCache; |
|
} |
|
} else { |
|
|
|
let lowerCache; |
|
if( !deqing && !highQualityReq && !downscaleReq ){ |
|
for( let l = lvl - 1; l >= minLvl; l-- ){ |
|
let c = lookup.get( ele, l ); |
|
|
|
if( c ){ lowerCache = c; break; } |
|
} |
|
} |
|
|
|
if( scalableFrom(lowerCache) ){ |
|
|
|
|
|
self.queueElement( ele, lvl ); |
|
|
|
return lowerCache; |
|
} |
|
|
|
txr.context.translate( txr.usedWidth, 0 ); |
|
txr.context.scale( scale, scale ); |
|
|
|
this.drawElement( txr.context, ele, bb, scaledLabelShown, false ); |
|
|
|
txr.context.scale( 1/scale, 1/scale ); |
|
txr.context.translate( -txr.usedWidth, 0 ); |
|
} |
|
|
|
eleCache = { |
|
x: txr.usedWidth, |
|
texture: txr, |
|
level: lvl, |
|
scale: scale, |
|
width: eleScaledW, |
|
height: eleScaledH, |
|
scaledLabelShown: scaledLabelShown |
|
}; |
|
|
|
txr.usedWidth += Math.ceil( eleScaledW + eleTxrSpacing ); |
|
|
|
txr.eleCaches.push( eleCache ); |
|
|
|
lookup.set( ele, lvl, eleCache ); |
|
|
|
self.checkTextureFullness( txr ); |
|
|
|
return eleCache; |
|
}; |
|
|
|
ETCp.invalidateElements = function( eles ){ |
|
for( let i = 0; i < eles.length; i++ ){ |
|
this.invalidateElement(eles[i]); |
|
} |
|
}; |
|
|
|
ETCp.invalidateElement = function( ele ){ |
|
let self = this; |
|
let lookup = self.lookup; |
|
let caches = []; |
|
let invalid = lookup.isInvalid(ele); |
|
|
|
if( !invalid ){ |
|
return; |
|
} |
|
|
|
for( let lvl = minLvl; lvl <= maxLvl; lvl++ ){ |
|
let cache = lookup.getForCachedKey( ele, lvl ); |
|
|
|
if( cache ){ |
|
caches.push( cache ); |
|
} |
|
} |
|
|
|
let noOtherElesUseCache = lookup.invalidate(ele); |
|
|
|
if( noOtherElesUseCache ){ |
|
for( let i = 0; i < caches.length; i++ ){ |
|
let cache = caches[i]; |
|
let txr = cache.texture; |
|
|
|
|
|
txr.invalidatedWidth += cache.width; |
|
|
|
|
|
cache.invalidated = true; |
|
|
|
|
|
self.checkTextureUtility( txr ); |
|
} |
|
} |
|
|
|
|
|
self.removeFromQueue( ele ); |
|
}; |
|
|
|
ETCp.checkTextureUtility = function( txr ){ |
|
|
|
if( txr.invalidatedWidth >= minUtility * txr.width ){ |
|
this.retireTexture( txr ); |
|
} |
|
}; |
|
|
|
ETCp.checkTextureFullness = function( txr ){ |
|
|
|
|
|
|
|
let self = this; |
|
let txrQ = self.getTextureQueue( txr.height ); |
|
|
|
if( txr.usedWidth / txr.width > maxFullness && txr.fullnessChecks >= maxFullnessChecks ){ |
|
removeFromArray( txrQ, txr ); |
|
} else { |
|
txr.fullnessChecks++; |
|
} |
|
}; |
|
|
|
ETCp.retireTexture = function( txr ){ |
|
let self = this; |
|
let txrH = txr.height; |
|
let txrQ = self.getTextureQueue( txrH ); |
|
let lookup = this.lookup; |
|
|
|
|
|
|
|
removeFromArray( txrQ, txr ); |
|
|
|
txr.retired = true; |
|
|
|
|
|
|
|
let eleCaches = txr.eleCaches; |
|
|
|
for( let i = 0; i < eleCaches.length; i++ ){ |
|
let eleCache = eleCaches[i]; |
|
|
|
lookup.deleteCache( eleCache.key, eleCache.level ); |
|
} |
|
|
|
clearArray( eleCaches ); |
|
|
|
|
|
|
|
let rtxtrQ = self.getRetiredTextureQueue( txrH ); |
|
|
|
rtxtrQ.push( txr ); |
|
}; |
|
|
|
ETCp.addTexture = function( txrH, minW ){ |
|
let self = this; |
|
let txrQ = self.getTextureQueue( txrH ); |
|
let txr = {}; |
|
|
|
txrQ.push( txr ); |
|
|
|
txr.eleCaches = []; |
|
|
|
txr.height = txrH; |
|
txr.width = Math.max( defTxrWidth, minW ); |
|
txr.usedWidth = 0; |
|
txr.invalidatedWidth = 0; |
|
txr.fullnessChecks = 0; |
|
|
|
txr.canvas = self.renderer.makeOffscreenCanvas(txr.width, txr.height); |
|
|
|
txr.context = txr.canvas.getContext('2d'); |
|
|
|
return txr; |
|
}; |
|
|
|
ETCp.recycleTexture = function( txrH, minW ){ |
|
let self = this; |
|
let txrQ = self.getTextureQueue( txrH ); |
|
let rtxtrQ = self.getRetiredTextureQueue( txrH ); |
|
|
|
for( let i = 0; i < rtxtrQ.length; i++ ){ |
|
let txr = rtxtrQ[i]; |
|
|
|
if( txr.width >= minW ){ |
|
txr.retired = false; |
|
|
|
txr.usedWidth = 0; |
|
txr.invalidatedWidth = 0; |
|
txr.fullnessChecks = 0; |
|
|
|
clearArray( txr.eleCaches ); |
|
|
|
txr.context.setTransform( 1, 0, 0, 1, 0, 0 ); |
|
txr.context.clearRect( 0, 0, txr.width, txr.height ); |
|
|
|
removeFromArray( rtxtrQ, txr ); |
|
txrQ.push( txr ); |
|
|
|
return txr; |
|
} |
|
} |
|
}; |
|
|
|
ETCp.queueElement = function( ele, lvl ){ |
|
let self = this; |
|
let q = self.getElementQueue(); |
|
let k2q = self.getElementKeyToQueue(); |
|
let key = this.getKey(ele); |
|
let existingReq = k2q[key]; |
|
|
|
if( existingReq ){ |
|
|
|
existingReq.level = Math.max( existingReq.level, lvl ); |
|
|
|
existingReq.eles.merge(ele); |
|
|
|
existingReq.reqs++; |
|
|
|
q.updateItem( existingReq ); |
|
} else { |
|
let req = { |
|
eles: ele.spawn().merge(ele), |
|
level: lvl, |
|
reqs: 1, |
|
key |
|
}; |
|
|
|
q.push( req ); |
|
|
|
k2q[key] = req; |
|
} |
|
}; |
|
|
|
ETCp.dequeue = function( pxRatio ){ |
|
let self = this; |
|
let q = self.getElementQueue(); |
|
let k2q = self.getElementKeyToQueue(); |
|
let dequeued = []; |
|
let lookup = self.lookup; |
|
|
|
for( let i = 0; i < maxDeqSize; i++ ){ |
|
if( q.size() > 0 ){ |
|
let req = q.pop(); |
|
let key = req.key; |
|
let ele = req.eles[0]; |
|
let cacheExists = lookup.hasCache(ele, req.level); |
|
|
|
|
|
k2q[key] = null; |
|
|
|
|
|
if( cacheExists ){ continue; } |
|
|
|
dequeued.push( req ); |
|
|
|
let bb = self.getBoundingBox( ele ); |
|
|
|
self.getElement( ele, bb, pxRatio, req.level, getTxrReasons.dequeue ); |
|
} else { |
|
break; |
|
} |
|
} |
|
|
|
return dequeued; |
|
}; |
|
|
|
ETCp.removeFromQueue = function( ele ){ |
|
let self = this; |
|
let q = self.getElementQueue(); |
|
let k2q = self.getElementKeyToQueue(); |
|
let key = this.getKey(ele); |
|
let req = k2q[key]; |
|
|
|
if( req != null ){ |
|
if( req.eles.length === 1 ){ |
|
|
|
req.reqs = MAX_INT; |
|
q.updateItem(req); |
|
|
|
q.pop(); |
|
|
|
k2q[key] = null; |
|
} else { |
|
req.eles.unmerge(ele); |
|
} |
|
} |
|
}; |
|
|
|
ETCp.onDequeue = function( fn ){ this.onDequeues.push( fn ); }; |
|
ETCp.offDequeue = function( fn ){ removeFromArray( this.onDequeues, fn ); }; |
|
|
|
ETCp.setupDequeueing = defs.setupDequeueing({ |
|
deqRedrawThreshold: deqRedrawThreshold, |
|
deqCost: deqCost, |
|
deqAvgCost: deqAvgCost, |
|
deqNoDrawCost: deqNoDrawCost, |
|
deqFastCost: deqFastCost, |
|
deq: function( self, pxRatio, extent ){ |
|
return self.dequeue( pxRatio, extent ); |
|
}, |
|
onDeqd: function( self, deqd ){ |
|
for( let i = 0; i < self.onDequeues.length; i++ ){ |
|
let fn = self.onDequeues[i]; |
|
|
|
fn( deqd ); |
|
} |
|
}, |
|
shouldRedraw: function( self, deqd, pxRatio, extent ){ |
|
for( let i = 0; i < deqd.length; i++ ){ |
|
let eles = deqd[i].eles; |
|
|
|
for( let j = 0; j < eles.length; j++ ){ |
|
let bb = eles[j].boundingBox(); |
|
|
|
if( math.boundingBoxesIntersect( bb, extent ) ){ |
|
return true; |
|
} |
|
} |
|
} |
|
|
|
return false; |
|
}, |
|
priority: function( self ){ |
|
return self.renderer.beforeRenderPriorities.eleTxrDeq; |
|
} |
|
}); |
|
|
|
export default ElementTextureCache; |
|
|