Merge pull request #659 from avandecreme/master

Move tile caching code inside tilecache.js.
This commit is contained in:
Ian Gilman 2015-06-12 16:23:11 -07:00
commit a3183183b2
14 changed files with 250 additions and 73 deletions

View File

@ -1,7 +1,8 @@
# editorconfig.org
root = true
[*]
# We need to specify each folder specifically to avoid including test/lib and test/data
[{Gruntfile.js,src/**,test/*,test/demo/**,test/helpers/**,test/modules/**}]
indent_style = space
indent_size = 4
end_of_line = lf

View File

@ -2,6 +2,11 @@ OPENSEADRAGON CHANGELOG
=======================
2.0.1: (in progress)
* BREAKING CHANGE: the tile does not hold a reference to its image anymore. Only the tile cache keep a reference to images.
* DEPRECATION: let ImageRecord.getRenderedContext create the rendered context instead of using ImageRecord.setRenderedContext.
* Added "tile-loaded" event on the viewer allowing to modify a tile before it is marked ready to be drawn. (#659)
* Added "tile-unloaded" event on the viewer allowing to free up memory one has allocated on a tile. (#659)
* Fix flickering tiles with useCanvas=false when no cache is used. (#661)
2.0.0:

View File

@ -68,7 +68,7 @@ $.ButtonGroup = function( options ) {
*/
this.element = options.element || $.makeNeutralElement( "div" );
// TODO What if there IS an options.group specified?
// TODO What if there IS an options.group specified?
if( !options.group ){
this.label = $.makeNeutralElement( "label" );
//TODO: support labels for ButtonGroups

View File

@ -94,7 +94,7 @@ $.IIIFTileSource = function( options ){
// If we're smaller than 256, just use the short side.
options.tileSize = shortDim;
}
this.tile_width = options.tileSize; // So that 'full' gets used for
this.tile_width = options.tileSize; // So that 'full' gets used for
this.tile_height = options.tileSize; // the region below
}

View File

@ -133,7 +133,7 @@
*/
this.element = $.getElement( options.element );
/**
* The number of milliseconds within which a pointer down-up event combination
* The number of milliseconds within which a pointer down-up event combination
* will be treated as a click gesture.
* @member {Number} clickTimeThreshold
* @memberof OpenSeadragon.MouseTracker#
@ -244,7 +244,7 @@
// Active pointers lists. Array of GesturePointList objects, one for each pointer device type.
// GesturePointList objects are added each time a pointer is tracked by a new pointer device type (see getActivePointersListByType()).
// Active pointers are any pointer being tracked for this element which are in the hit-test area
// Active pointers are any pointer being tracked for this element which are in the hit-test area
// of the element (for hover-capable devices) and/or have contact or a button press initiated in the element.
activePointersLists: [],
@ -1032,7 +1032,7 @@
$.MouseTracker.mousePointerId = "legacy-mouse";
$.MouseTracker.maxTouchPoints = 10;
}
///////////////////////////////////////////////////////////////////////////////
// Classes and typedefs
@ -1078,7 +1078,7 @@
/**
* @class GesturePointList
* @classdesc Provides an abstraction for a set of active {@link OpenSeadragon.MouseTracker.GesturePoint|GesturePoint} objects for a given pointer device type.
* Active pointers are any pointer being tracked for this element which are in the hit-test area
* Active pointers are any pointer being tracked for this element which are in the hit-test area
* of the element (for hover-capable devices) and/or have contact or a button press initiated in the element.
* @memberof OpenSeadragon.MouseTracker
* @param {String} type - The pointer device type: "mouse", "touch", "pen", etc.
@ -1198,7 +1198,7 @@
return null;
}
};
///////////////////////////////////////////////////////////////////////////////
// Utility functions
@ -1282,7 +1282,7 @@
false
);
}
clearTrackedPointers( tracker );
delegate.tracking = true;
@ -1694,7 +1694,7 @@
/**
* Handles 'wheel' events.
* Handles 'wheel' events.
* The event may be simulated by the legacy mouse wheel event handler (onMouseWheel()).
*
* @private
@ -1943,7 +1943,7 @@
handleMouseMove( tracker, event );
}
/**
* This handler is attached to the window object (on the capture phase) to emulate mouse capture.
* onMouseMove is still attached to the tracked element, so stop propagation to avoid processing twice.
@ -2191,7 +2191,7 @@
var i,
touchCount = event.changedTouches.length,
gPoints = [];
for ( i = 0; i < touchCount; i++ ) {
gPoints.push( {
id: event.changedTouches[ i ].identifier,
@ -2420,7 +2420,7 @@
*/
function startTrackingPointer( pointsList, gPoint ) {
// If isPrimary is not known for the pointer then set it according to our rules:
// If isPrimary is not known for the pointer then set it according to our rules:
// true if the first pointer in the gesture, otherwise false
if ( !gPoint.hasOwnProperty( 'isPrimary' ) ) {
if ( pointsList.getLength() === 0 ) {
@ -2617,7 +2617,7 @@
* Gesture points associated with the event.
* @param {Number} buttonChanged
* The button involved in the event: -1: none, 0: primary/left, 1: aux/middle, 2: secondary/right, 3: X1/back, 4: X2/forward, 5: pen eraser.
* Note on chorded button presses (a button pressed when another button is already pressed): In the W3C Pointer Events model,
* Note on chorded button presses (a button pressed when another button is already pressed): In the W3C Pointer Events model,
* only one pointerdown/pointerup event combo is fired. Chorded button state changes instead fire pointermove events.
*
* @returns {Boolean} True if pointers should be captured to the tracked element, otherwise false.
@ -2779,7 +2779,7 @@
* Gesture points associated with the event.
* @param {Number} buttonChanged
* The button involved in the event: -1: none, 0: primary/left, 1: aux/middle, 2: secondary/right, 3: X1/back, 4: X2/forward, 5: pen eraser.
* Note on chorded button presses (a button pressed when another button is already pressed): In the W3C Pointer Events model,
* Note on chorded button presses (a button pressed when another button is already pressed): In the W3C Pointer Events model,
* only one pointerdown/pointerup event combo is fired. Chorded button state changes instead fire pointermove events.
*
* @returns {Boolean} True if pointer capture should be released from the tracked element, otherwise false.

View File

@ -190,7 +190,14 @@ $.Tile.prototype = /** @lends OpenSeadragon.Tile.prototype */{
* @param {Element} container
*/
drawHTML: function( container ) {
if ( !this.loaded || !this.image ) {
if (!this.cacheImageRecord) {
$.console.warn(
'[Tile.drawHTML] attempting to draw tile %s when it\'s not cached',
this.toString());
return;
}
if ( !this.loaded ) {
$.console.warn(
"Attempting to draw tile %s when it's not yet loaded.",
this.toString()
@ -203,8 +210,7 @@ $.Tile.prototype = /** @lends OpenSeadragon.Tile.prototype */{
if ( !this.element ) {
this.element = $.makeNeutralElement( "div" );
this.imgElement = $.makeNeutralElement( "img" );
this.imgElement.src = this.url;
this.imgElement = this.cacheImageRecord.getImage().cloneNode();
this.imgElement.style.msInterpolationMode = "nearest-neighbor";
this.imgElement.style.width = "100%";
this.imgElement.style.height = "100%";
@ -239,17 +245,18 @@ $.Tile.prototype = /** @lends OpenSeadragon.Tile.prototype */{
var position = this.position,
size = this.size,
rendered,
canvas;
rendered;
if (!this.cacheImageRecord) {
$.console.warn('[Tile.drawCanvas] attempting to draw tile %s when it\'s not cached', this.toString());
$.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) ){
if ( !this.loaded || !rendered ){
$.console.warn(
"Attempting to draw tile %s when it's not yet loaded.",
this.toString()
@ -276,19 +283,8 @@ $.Tile.prototype = /** @lends OpenSeadragon.Tile.prototype */{
}
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 );
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;
}
// This gives the application a chance to make image manipulation changes as we are rendering the image
// This gives the application a chance to make image manipulation
// changes as we are rendering the image
drawingHandler({context: context, tile: this, rendered: rendered});
context.drawImage(
@ -318,7 +314,6 @@ $.Tile.prototype = /** @lends OpenSeadragon.Tile.prototype */{
this.element = null;
this.imgElement = null;
this.image = null;
this.loaded = false;
this.loading = false;
}

View File

@ -63,10 +63,23 @@ ImageRecord.prototype = {
},
getRenderedContext: function() {
if (!this._renderedContext) {
var canvas = document.createElement( 'canvas' );
canvas.width = this._image.width;
canvas.height = this._image.height;
this._renderedContext = canvas.getContext('2d');
this._renderedContext.drawImage( this._image, 0, 0 );
//since we are caching the prerendered image on a canvas
//allow the image to not be held in memory
this._image = null;
}
return this._renderedContext;
},
setRenderedContext: function(renderedContext) {
$.console.error("ImageRecord.setRenderedContext is deprecated. " +
"The rendered context should be created by the ImageRecord " +
"itself when calling ImageRecord.getRenderedContext.");
this._renderedContext = renderedContext;
},
@ -126,6 +139,7 @@ $.TileCache.prototype = /** @lends OpenSeadragon.TileCache.prototype */{
* 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 {Image} options.image - The image of the tile to cache.
* @param {OpenSeadragon.TiledImage} options.tiledImage - The TiledImage that owns that tile.
* @param {Number} [options.cutoff=0] - If adding this tile goes over the cache max count, this
* function will release an old tile. The cutoff option specifies a tile level at or below which
@ -135,7 +149,7 @@ $.TileCache.prototype = /** @lends OpenSeadragon.TileCache.prototype */{
$.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.image, "[TileCache.cacheTile] options.image is required" );
$.console.assert( options.tiledImage, "[TileCache.cacheTile] options.tiledImage is required" );
var cutoff = options.cutoff || 0;
@ -144,7 +158,7 @@ $.TileCache.prototype = /** @lends OpenSeadragon.TileCache.prototype */{
var imageRecord = this._imagesLoaded[options.tile.url];
if (!imageRecord) {
imageRecord = this._imagesLoaded[options.tile.url] = new ImageRecord({
image: options.tile.image
image: options.image
});
this._imagesLoadedCount++;
@ -158,6 +172,7 @@ $.TileCache.prototype = /** @lends OpenSeadragon.TileCache.prototype */{
if ( this._imagesLoadedCount > this._maxImageCacheCount ) {
var worstTile = null;
var worstTileIndex = -1;
var worstTileRecord = null;
var prevTile, worstTime, worstLevel, prevTime, prevLevel, prevTileRecord;
for ( var i = this._tilesLoaded.length - 1; i >= 0; i-- ) {
@ -169,6 +184,7 @@ $.TileCache.prototype = /** @lends OpenSeadragon.TileCache.prototype */{
} else if ( !worstTile ) {
worstTile = prevTile;
worstTileIndex = i;
worstTileRecord = prevTileRecord;
continue;
}
@ -181,11 +197,12 @@ $.TileCache.prototype = /** @lends OpenSeadragon.TileCache.prototype */{
( prevTime == worstTime && prevLevel > worstLevel ) ) {
worstTile = prevTile;
worstTileIndex = i;
worstTileRecord = prevTileRecord;
}
}
if ( worstTile && worstTileIndex >= 0 ) {
this._unloadTile(worstTile);
this._unloadTile(worstTileRecord);
insertionIndex = worstTileIndex;
}
}
@ -206,7 +223,7 @@ $.TileCache.prototype = /** @lends OpenSeadragon.TileCache.prototype */{
for ( var i = 0; i < this._tilesLoaded.length; ++i ) {
tileRecord = this._tilesLoaded[ i ];
if ( tileRecord.tiledImage === tiledImage ) {
this._unloadTile(tileRecord.tile);
this._unloadTile(tileRecord);
this._tilesLoaded.splice( i, 1 );
i--;
}
@ -220,8 +237,11 @@ $.TileCache.prototype = /** @lends OpenSeadragon.TileCache.prototype */{
},
// private
_unloadTile: function(tile) {
$.console.assert(tile, '[TileCache._unloadTile] tile is required');
_unloadTile: function(tileRecord) {
$.console.assert(tileRecord, '[TileCache._unloadTile] tileRecord is required');
var tile = tileRecord.tile;
var tiledImage = tileRecord.tiledImage;
tile.unload();
tile.cacheImageRecord = null;
@ -232,6 +252,20 @@ $.TileCache.prototype = /** @lends OpenSeadragon.TileCache.prototype */{
delete this._imagesLoaded[tile.url];
this._imagesLoadedCount--;
}
/**
* Triggered when a tile has just been unloaded from memory.
*
* @event tile-unloaded
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {OpenSeadragon.TiledImage} tiledImage - The tiled image of the unloaded tile.
* @property {OpenSeadragon.Tile} tile - The tile which has been unloaded.
*/
tiledImage.viewer.raiseEvent("tile-unloaded", {
tile: tile,
tiledImage: tiledImage
});
}
};

View File

@ -715,8 +715,6 @@ function updateViewport( tiledImage ) {
// Load the new 'best' tile
if ( best ) {
loadTile( tiledImage, best, currentTime );
// because we haven't finished drawing, so
tiledImage._needsDraw = true;
}
}
@ -862,13 +860,8 @@ function updateTile( tiledImage, drawLevel, haveDrawn, x, y, level, levelOpacity
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
});
var image = imageRecord.getImage();
setTileLoaded(tiledImage, tile, image);
}
}
@ -965,16 +958,9 @@ function onTileLoad( tiledImage, tile, time, image ) {
}
var finish = function() {
tile.loading = false;
tile.loaded = true;
tile.image = image;
var cutoff = Math.ceil( Math.log( tiledImage.source.getTileSize(tile.level) ) / Math.log( 2 ) );
tiledImage._tileCache.cacheTile({
tile: tile,
cutoff: cutoff,
tiledImage: tiledImage
});
var cutoff = Math.ceil( Math.log(
tiledImage.source.getTileSize(tile.level) ) / Math.log( 2 ) );
setTileLoaded(tiledImage, tile, image, cutoff);
};
// Check if we're mid-update; this can happen on IE8 because image load events for
@ -985,10 +971,55 @@ function onTileLoad( tiledImage, tile, time, image ) {
// Wait until after the update, in case caching unloads any tiles
window.setTimeout( finish, 1);
}
tiledImage._needsDraw = true;
}
function setTileLoaded(tiledImage, tile, image, cutoff) {
var increment = 0;
function getCompletionCallback() {
increment++;
return completionCallback;
}
function completionCallback() {
increment--;
if (increment === 0) {
tile.loading = false;
tile.loaded = true;
tiledImage._tileCache.cacheTile({
image: image,
tile: tile,
cutoff: cutoff,
tiledImage: tiledImage
});
tiledImage._needsDraw = true;
}
}
/**
* Triggered when a tile has just been loaded in memory. That means that the
* image has been downloaded and can be modified before being drawn to the canvas.
*
* @event tile-loaded
* @memberof OpenSeadragon.Viewer
* @type {object}
* @property {Image} image - The image of the tile.
* @property {OpenSeadragon.TiledImage} tiledImage - The tiled image of the loaded tile.
* @property {OpenSeadragon.Tile} tile - The tile which has been loaded.
* @property {function} getCompletionCallback - A function giving a callback to call
* when the asynchronous processing of the image is done. The image will be
* marked as entirely loaded when the callback has been called once for each
* call to getCompletionCallback.
*/
tiledImage.viewer.raiseEvent("tile-loaded", {
tile: tile,
tiledImage: tiledImage,
image: image,
getCompletionCallback: getCompletionCallback
});
// In case the completion callback is never called, we at least force it once.
getCompletionCallback()();
}
function positionTile( tile, overlap, viewport, viewportCenter, levelVisibility, tiledImage ){
var boundsTL = tile.bounds.getTopLeft();

View File

@ -23,19 +23,19 @@
<script type="text/javascript">
var _viewer;
var generateUniqueHash = (function() {
var counter = 0;
return function() {
return "openseadragon_" + (counter++);
};
})();
function createViewer() {
if ( _viewer ) {
destroyViewer();
}
_viewer = OpenSeadragon({
element: document.getElementById("contentDiv"),
showNavigationControl: false,
@ -44,7 +44,7 @@
tileSources: "../data/testpattern.dzi"
});
}
function destroyViewer() {
if ( _viewer ) {
_viewer.destroy();

File diff suppressed because one or more lines are too long

View File

@ -968,4 +968,103 @@
viewer.open( '/test/data/testpattern.dzi' );
} );
// tile-loaded event tests
asyncTest( 'Viewer: tile-loaded event without callback.', function () {
function tileLoaded ( event ) {
viewer.removeHandler( 'tile-loaded', tileLoaded);
var tile = event.tile;
ok( tile.loading, "The tile should be marked as loading.");
notOk( tile.loaded, "The tile should not be marked as loaded.");
setTimeout(function() {
notOk( tile.loading, "The tile should not be marked as loading.");
ok( tile.loaded, "The tile should be marked as loaded.");
start();
}, 0);
}
viewer.addHandler( 'tile-loaded', tileLoaded);
viewer.open( '/test/data/testpattern.dzi' );
} );
asyncTest( 'Viewer: tile-loaded event with 1 callback.', function () {
function tileLoaded ( event ) {
viewer.removeHandler( 'tile-loaded', tileLoaded);
var tile = event.tile;
var callback = event.getCompletionCallback();
ok( tile.loading, "The tile should be marked as loading.");
notOk( tile.loaded, "The tile should not be marked as loaded.");
ok( callback, "The event should have a callback.");
setTimeout(function() {
ok( tile.loading, "The tile should be marked as loading.");
notOk( tile.loaded, "The tile should not be marked as loaded.");
callback();
notOk( tile.loading, "The tile should not be marked as loading.");
ok( tile.loaded, "The tile should be marked as loaded.");
start();
}, 0);
}
viewer.addHandler( 'tile-loaded', tileLoaded);
viewer.open( '/test/data/testpattern.dzi' );
} );
asyncTest( 'Viewer: tile-loaded event with 2 callbacks.', function () {
function tileLoaded ( event ) {
viewer.removeHandler( 'tile-loaded', tileLoaded);
var tile = event.tile;
var callback1 = event.getCompletionCallback();
var callback2 = event.getCompletionCallback();
ok( tile.loading, "The tile should be marked as loading.");
notOk( tile.loaded, "The tile should not be marked as loaded.");
setTimeout(function() {
ok( tile.loading, "The tile should be marked as loading.");
notOk( tile.loaded, "The tile should not be marked as loaded.");
callback1();
ok( tile.loading, "The tile should be marked as loading.");
notOk( tile.loaded, "The tile should not be marked as loaded.");
setTimeout(function() {
ok( tile.loading, "The tile should be marked as loading.");
notOk( tile.loaded, "The tile should not be marked as loaded.");
callback2();
notOk( tile.loading, "The tile should not be marked as loading.");
ok( tile.loaded, "The tile should be marked as loaded.");
start();
}, 0);
}, 0);
}
viewer.addHandler( 'tile-loaded', tileLoaded);
viewer.open( '/test/data/testpattern.dzi' );
} );
asyncTest( 'Viewer: tile-unloaded event.', function() {
var tiledImage;
var tile;
function tileLoaded( event ) {
viewer.removeHandler( 'tile-loaded', tileLoaded);
tiledImage = event.tiledImage;
tile = event.tile;
setTimeout(function() {
tiledImage.reset();
}, 0);
}
function tileUnloaded( event ) {
viewer.removeHandler( 'tile-unloaded', tileUnloaded );
equal( tile, event.tile,
"The unloaded tile should be the same than the loaded one." );
equal( tiledImage, event.tiledImage,
"The tiledImage of the unloaded tile should be the same than the one of the loaded one." );
start();
}
viewer.addHandler( 'tile-loaded', tileLoaded );
viewer.addHandler( 'tile-unloaded', tileUnloaded );
viewer.open( '/test/data/testpattern.dzi' );
} );
} )();

View File

@ -108,6 +108,6 @@
// ----------
asyncTest('IIIF 2.0 JSON', function() {
testOpen('iiif_2_0_tiled/info.json');
});
});
})();

View File

@ -13,8 +13,15 @@
// ----------
asyncTest('basics', function() {
var fakeTiledImage0 = {};
var fakeTiledImage1 = {};
var fakeViewer = {
raiseEvent: function() {}
};
var fakeTiledImage0 = {
viewer: fakeViewer
};
var fakeTiledImage1 = {
viewer: fakeViewer
};
var fakeTile0 = {
url: 'foo.jpg',
@ -58,7 +65,12 @@
// ----------
asyncTest('maxImageCacheCount', function() {
var fakeTiledImage0 = {};
var fakeViewer = {
raiseEvent: function() {}
};
var fakeTiledImage0 = {
viewer: fakeViewer
};
var fakeTile0 = {
url: 'different.jpg',

View File

@ -107,7 +107,7 @@
var actualImageZoom = viewport.viewportToImageZoom(
expectedViewportZoom);
equal(actualImageZoom, expectedImageZoom);
var actualViewportZoom = viewport.imageToViewportZoom(actualImageZoom);
equal(actualViewportZoom, expectedViewportZoom);
}