From 864127989047282808cdcad590b305a9d4d57ed0 Mon Sep 17 00:00:00 2001 From: Ian Gilman Date: Wed, 5 Nov 2014 13:48:27 -0800 Subject: [PATCH] Better tile caching for duplicate images --- src/tile.js | 28 +++++----- src/tilecache.js | 96 +++++++++++++++++++++++++++++++++-- src/tiledimage.js | 13 +++++ test/demo/collections/main.js | 32 +++++++----- 4 files changed, 137 insertions(+), 32 deletions(-) diff --git a/src/tile.js b/src/tile.js index 9aa4bde4..b54e9b32 100644 --- a/src/tile.js +++ b/src/tile.js @@ -33,7 +33,7 @@ */ (function( $ ){ - var TILE_CACHE = {}; + /** * @class Tile * @memberof OpenSeadragon @@ -241,16 +241,23 @@ $.Tile.prototype = /** @lends OpenSeadragon.Tile.prototype */{ rendered, canvas; - if ( !this.loaded || !( this.image || TILE_CACHE[ this.url ] ) ){ + if (!this.cacheImageRecord) { + $.console.warn('[Tile.drawCanvas] attempting to draw tile %s when it\'s not cached', this.toString()); + return; + } + + rendered = this.cacheImageRecord.getRenderedContext(); + + if ( !this.loaded || !( this.image || rendered) ){ $.console.warn( "Attempting to draw tile %s when it's not yet loaded.", this.toString() ); + return; } - context.globalAlpha = this.opacity; - //context.save(); + context.globalAlpha = this.opacity; //if we are supposed to be rendering fully opaque rectangle, //ie its done fading or fading is turned off, and if we are drawing @@ -268,24 +275,21 @@ $.Tile.prototype = /** @lends OpenSeadragon.Tile.prototype */{ } - if( !TILE_CACHE[ this.url ] ){ + if(!rendered){ canvas = document.createElement( 'canvas' ); canvas.width = this.image.width; canvas.height = this.image.height; rendered = canvas.getContext('2d'); rendered.drawImage( this.image, 0, 0 ); - TILE_CACHE[ this.url ] = rendered; + this.cacheImageRecord.setRenderedContext(rendered); //since we are caching the prerendered image on a canvas //allow the image to not be held in memory this.image = null; } - rendered = TILE_CACHE[ this.url ]; - // This gives the application a chance to make image manipulation changes as we are rendering the image drawingHandler({context: context, tile: this, rendered: rendered}); - //rendered.save(); context.drawImage( rendered.canvas, 0, @@ -297,9 +301,6 @@ $.Tile.prototype = /** @lends OpenSeadragon.Tile.prototype */{ size.x, size.y ); - //rendered.restore(); - - //context.restore(); }, /** @@ -313,9 +314,6 @@ $.Tile.prototype = /** @lends OpenSeadragon.Tile.prototype */{ if ( this.element && this.element.parentNode ) { this.element.parentNode.removeChild( this.element ); } - if ( TILE_CACHE[ this.url ]){ - delete TILE_CACHE[ this.url ]; - } this.element = null; this.imgElement = null; diff --git a/src/tilecache.js b/src/tilecache.js index 71a8f25a..627ecc54 100644 --- a/src/tilecache.js +++ b/src/tilecache.js @@ -43,6 +43,53 @@ var TileRecord = function( options ) { this.tiledImage = options.tiledImage; }; +// private +var ImageRecord = function(options) { + $.console.assert( options, "[ImageRecord] options is required" ); + $.console.assert( options.image, "[ImageRecord] options.image is required" ); + this._image = options.image; + this._tiles = []; +}; + +ImageRecord.prototype = { + destroy: function() { + this._image = null; + this._renderedContext = null; + this._tiles = null; + }, + + getImage: function() { + return this._image; + }, + + getRenderedContext: function() { + return this._renderedContext; + }, + + setRenderedContext: function(renderedContext) { + this._renderedContext = renderedContext; + }, + + addTile: function(tile) { + this._tiles.push(tile); + }, + + removeTile: function(tile) { + for (var i = 0; i < this._tiles.length; i++) { + if (this._tiles[i] === tile) { + this._tiles.splice(i, 1); + return; + } + } + + $.console.warn('[ImageRecord.removeTile] trying to remove unknown tile', tile); + }, + + getTileCount: function() { + return this._tiles.length; + } +}; + /** * @class TileCache * @memberof OpenSeadragon @@ -54,8 +101,10 @@ var TileRecord = function( options ) { $.TileCache = function( options ) { options = options || {}; - this._tilesLoaded = []; this._maxImageCacheCount = options.maxImageCacheCount || $.DEFAULT_SETTINGS.maxImageCacheCount; + this._tilesLoaded = []; + this._imagesLoaded = []; + this._imagesLoadedCount = 0; }; $.TileCache.prototype = /** @lends OpenSeadragon.TileCache.prototype */{ @@ -69,7 +118,10 @@ $.TileCache.prototype = /** @lends OpenSeadragon.TileCache.prototype */{ /** * Caches the specified tile, removing an old tile if necessary to stay under the - * maxImageCacheCount specified on construction. + * maxImageCacheCount specified on construction. Note that if multiple tiles reference + * the same image, there may be more tiles than maxImageCacheCount; the goal is to keep + * the number of images below that number. Note, as well, that even the number of images + * may temporarily surpass that number, but should eventually come back down to the max specified. * @param {Object} options - Tile info. * @param {OpenSeadragon.Tile} options.tile - The tile to cache. * @param {OpenSeadragon.TiledImage} options.tiledImage - The TiledImage that owns that tile. @@ -80,12 +132,28 @@ $.TileCache.prototype = /** @lends OpenSeadragon.TileCache.prototype */{ cacheTile: function( options ) { $.console.assert( options, "[TileCache.cacheTile] options is required" ); $.console.assert( options.tile, "[TileCache.cacheTile] options.tile is required" ); + $.console.assert( options.tile.url, "[TileCache.cacheTile] options.tile.url is required" ); + $.console.assert( options.tile.image, "[TileCache.cacheTile] options.tile.image is required" ); $.console.assert( options.tiledImage, "[TileCache.cacheTile] options.tiledImage is required" ); var cutoff = options.cutoff || 0; var insertionIndex = this._tilesLoaded.length; - if ( this._tilesLoaded.length >= this._maxImageCacheCount ) { + var imageRecord = this._imagesLoaded[options.tile.url]; + if (!imageRecord) { + imageRecord = this._imagesLoaded[options.tile.url] = new ImageRecord({ + image: options.tile.image + }); + + this._imagesLoadedCount++; + } + + imageRecord.addTile(options.tile); + options.tile.cacheImageRecord = imageRecord; + + // Note that just because we're unloading a tile doesn't necessarily mean + // we're unloading an image. With repeated calls it should sort itself out, though. + if ( this._imagesLoadedCount >= this._maxImageCacheCount ) { var worstTile = null; var worstTileIndex = -1; var prevTile, worstTime, worstLevel, prevTime, prevLevel, prevTileRecord; @@ -115,7 +183,7 @@ $.TileCache.prototype = /** @lends OpenSeadragon.TileCache.prototype */{ } if ( worstTile && worstTileIndex >= 0 ) { - worstTile.unload(); + this._unloadTile(worstTile); insertionIndex = worstTileIndex; } } @@ -134,11 +202,29 @@ $.TileCache.prototype = /** @lends OpenSeadragon.TileCache.prototype */{ for ( var i = 0; i < this._tilesLoaded.length; ++i ) { tileRecord = this._tilesLoaded[ i ]; if ( tileRecord.tiledImage === tiledImage ) { - tileRecord.tile.unload(); + this._unloadTile(tileRecord.tile); this._tilesLoaded.splice( i, 1 ); i--; } } + }, + + getImageRecord: function(url) { + return this._imagesLoaded[url]; + }, + + // private + _unloadTile: function(tile) { + tile.unload(); + tile.cacheImageRecord = null; + + var imageRecord = this._imagesLoaded[tile.url]; + imageRecord.removeTile(tile); + if (!imageRecord.getTileCount()) { + imageRecord.destroy(); + delete this._imagesLoaded[tile.url]; + this._imagesLoadedCount--; + } } }; diff --git a/src/tiledimage.js b/src/tiledimage.js index dea44ef0..6d169d40 100644 --- a/src/tiledimage.js +++ b/src/tiledimage.js @@ -485,6 +485,19 @@ function updateTile( tiledImage, drawLevel, haveDrawn, x, y, level, levelOpacity tiledImage ); + if (!tile.loaded) { + var imageRecord = tiledImage._tileCache.getImageRecord(tile.url); + if (imageRecord) { + tile.loaded = true; + tile.image = imageRecord.getImage(); + + tiledImage._tileCache.cacheTile({ + tile: tile, + tiledImage: tiledImage + }); + } + } + if ( tile.loaded ) { var needsUpdate = blendTile( tiledImage, diff --git a/test/demo/collections/main.js b/test/demo/collections/main.js index f558fa66..2479166a 100644 --- a/test/demo/collections/main.js +++ b/test/demo/collections/main.js @@ -9,12 +9,15 @@ var testInitialOpen = false; var testOverlays = false; var testMargins = false; + var testNavigator = false; var margins; var config = { debugMode: true, zoomPerScroll: 1.02, - showNavigator: true, + showNavigator: testNavigator, + wrapHorizontal: true, + wrapVertical: true, id: "contentDiv", prefixUrl: "../../../build/openseadragon/images/" }; @@ -83,7 +86,7 @@ } // this.crossTest3(); - this.basicTest(); + this.crossTest2(); }, // ---------- @@ -119,7 +122,8 @@ self.viewer.addTiledImage( options ); }); - this.viewer.open("../../data/tall.dzi", { + this.viewer.open({ + tileSource: "../../data/tall.dzi", x: 1.5, y: 0, width: 1 @@ -129,12 +133,13 @@ // ---------- crossTest2: function() { this.viewer.open([ + // { + // tileSource: "../../data/tall.dzi", + // x: 1.5, + // y: 0, + // width: 1 + // }, { - tileSource: "../../data/tall.dzi", - x: 1.5, - y: 0, - width: 1 - }, { tileSource: '../../data/wide.dzi', opacity: 1, x: 0, @@ -184,7 +189,7 @@ self.viewer.world.addHandler('add-item', function() { loaded++; if (loaded === expected) { - self.viewer.viewport.goHome(); + self.viewer.viewport.goHome(true); } }); @@ -208,7 +213,8 @@ } }); - this.viewer.open("../../data/testpattern.dzi", { + this.viewer.open({ + tileSource: "../../data/testpattern.dzi", x: startX, y: 0, width: 1 @@ -217,7 +223,8 @@ // ---------- bigTest: function() { - this.viewer.open("../../data/testpattern.dzi", { + this.viewer.open({ + tileSource: "../../data/testpattern.dzi", x: -2, y: -2, width: 6 @@ -246,7 +253,8 @@ } }; - this.viewer.open(dzi, { + this.viewer.open({ + tileSource: dzi, width: 100 }); },