Added ImageLoader; loads batches of images using async queue pattern

This commit is contained in:
Ryan Lester 2014-05-04 19:11:50 -07:00
parent 6467c0d0e9
commit dd51df97ab
3 changed files with 201 additions and 113 deletions

View File

@ -45,6 +45,7 @@ module.exports = function(grunt) {
"src/referencestrip.js", "src/referencestrip.js",
"src/displayrectangle.js", "src/displayrectangle.js",
"src/spring.js", "src/spring.js",
"src/imageLoader.js",
"src/tile.js", "src/tile.js",
"src/overlay.js", "src/overlay.js",
"src/drawer.js", "src/drawer.js",
@ -73,11 +74,11 @@ module.exports = function(grunt) {
}, },
concat: { concat: {
options: { options: {
banner: "//! <%= pkg.name %> <%= pkg.version %>\n" banner: "//! <%= pkg.name %> <%= pkg.version %>\n" +
+ "//! Built on <%= grunt.template.today('yyyy-mm-dd') %>\n" "//! Built on <%= grunt.template.today('yyyy-mm-dd') %>\n" +
+ "//! Git commit: <%= gitInfo %>\n" "//! Git commit: <%= gitInfo %>\n" +
+ "//! http://openseadragon.github.io\n" "//! http://openseadragon.github.io\n" +
+ "//! License: http://openseadragon.github.io/license/\n\n", "//! License: http://openseadragon.github.io/license/\n\n",
process: true process: true
}, },
dist: { dist: {
@ -182,9 +183,9 @@ module.exports = function(grunt) {
// Creates a directory tree to be compressed into a package. // Creates a directory tree to be compressed into a package.
grunt.registerTask("copy:package", function() { grunt.registerTask("copy:package", function() {
grunt.file.recurse("build/openseadragon", function(abspath, rootdir, subdir, filename) { grunt.file.recurse("build/openseadragon", function(abspath, rootdir, subdir, filename) {
var dest = packageDir var dest = packageDir +
+ (subdir ? subdir + "/" : '/') (subdir ? subdir + "/" : '/') +
+ filename; filename;
grunt.file.copy(abspath, dest); grunt.file.copy(abspath, dest);
}); });
grunt.file.copy("changelog.txt", packageDir + "changelog.txt"); grunt.file.copy("changelog.txt", packageDir + "changelog.txt");
@ -200,9 +201,9 @@ module.exports = function(grunt) {
return; return;
} }
var dest = releaseRoot var dest = releaseRoot +
+ (subdir ? subdir + "/" : '/') (subdir ? subdir + "/" : '/') +
+ filename; filename;
grunt.file.copy(abspath, dest); grunt.file.copy(abspath, dest);
}); });

View File

@ -76,7 +76,7 @@ $.Drawer = function( options ) {
//internal state properties //internal state properties
viewer: null, viewer: null,
downloading: 0, // How many images are currently being loaded in parallel. imageLoader: new $.ImageLoader(),
tilesMatrix: {}, // A '3d' dictionary [level][x][y] --> Tile. tilesMatrix: {}, // A '3d' dictionary [level][x][y] --> Tile.
tilesLoaded: [], // An unordered list of Tiles with loaded images. tilesLoaded: [], // An unordered list of Tiles with loaded images.
coverage: {}, // A '3d' dictionary [level][x][y] --> Boolean. coverage: {}, // A '3d' dictionary [level][x][y] --> Boolean.
@ -92,7 +92,6 @@ $.Drawer = function( options ) {
//configurable settings //configurable settings
opacity: $.DEFAULT_SETTINGS.opacity, opacity: $.DEFAULT_SETTINGS.opacity,
maxImageCacheCount: $.DEFAULT_SETTINGS.maxImageCacheCount, maxImageCacheCount: $.DEFAULT_SETTINGS.maxImageCacheCount,
imageLoaderLimit: $.DEFAULT_SETTINGS.imageLoaderLimit,
minZoomImageRatio: $.DEFAULT_SETTINGS.minZoomImageRatio, minZoomImageRatio: $.DEFAULT_SETTINGS.minZoomImageRatio,
wrapHorizontal: $.DEFAULT_SETTINGS.wrapHorizontal, wrapHorizontal: $.DEFAULT_SETTINGS.wrapHorizontal,
wrapVertical: $.DEFAULT_SETTINGS.wrapVertical, wrapVertical: $.DEFAULT_SETTINGS.wrapVertical,
@ -296,77 +295,6 @@ $.Drawer.prototype = /** @lends OpenSeadragon.Drawer.prototype */{
return this; return this;
}, },
/**
* Used internally to load images when required. May also be used to
* preload a set of images so the browser will have them available in
* the local cache to optimize user experience in certain cases. Because
* the number of parallel image loads is configurable, if too many images
* are currently being loaded, the request will be ignored. Since by
* default drawer.imageLoaderLimit is 0, the native browser parallel
* image loading policy will be used.
* @method
* @param {String} src - The url of the image to load.
* @param {Function} callback - The function that will be called with the
* Image object as the only parameter if it was loaded successfully.
* If an error occured, or the request timed out or was aborted,
* the parameter is null instead.
* @return {Boolean} loading - Whether the request was submitted or ignored
* based on OpenSeadragon.DEFAULT_SETTINGS.imageLoaderLimit.
*/
loadImage: function( src, callback ) {
var _this = this,
loading = false,
image,
jobid,
complete;
if ( !this.imageLoaderLimit ||
this.downloading < this.imageLoaderLimit ) {
this.downloading++;
image = new Image();
if ( _this.crossOriginPolicy !== false ) {
image.crossOrigin = _this.crossOriginPolicy;
}
complete = function( imagesrc, resultingImage ){
_this.downloading--;
if (typeof ( callback ) == "function") {
try {
callback( resultingImage );
} catch ( e ) {
$.console.error(
"%s while executing %s callback: %s",
e.name,
src,
e.message,
e
);
}
}
};
image.onload = function(){
finishLoadingImage( image, complete, true, jobid );
};
image.onabort = image.onerror = function(){
finishLoadingImage( image, complete, false, jobid );
};
jobid = window.setTimeout( function(){
finishLoadingImage( image, complete, false, jobid );
}, this.timeout );
loading = true;
image.src = src;
}
return loading;
},
/** /**
* Returns whether rotation is supported or not. * Returns whether rotation is supported or not.
* @method * @method
@ -436,13 +364,13 @@ function updateViewport( drawer ) {
levelOpacity, levelOpacity,
levelVisibility; levelVisibility;
//TODO // Reset tile's internal drawn state
while ( drawer.lastDrawn.length > 0 ) { while ( drawer.lastDrawn.length > 0 ) {
tile = drawer.lastDrawn.pop(); tile = drawer.lastDrawn.pop();
tile.beingDrawn = false; tile.beingDrawn = false;
} }
//TODO // Clear canvas
drawer.canvas.innerHTML = ""; drawer.canvas.innerHTML = "";
if ( drawer.useCanvas ) { if ( drawer.useCanvas ) {
if( drawer.canvas.width != viewportSize.x || if( drawer.canvas.width != viewportSize.x ||
@ -470,7 +398,7 @@ function updateViewport( drawer ) {
return; return;
} }
//TODO // Calculate viewport rect / bounds
if ( !drawer.wrapHorizontal ) { if ( !drawer.wrapHorizontal ) {
viewportTL.x = Math.max( viewportTL.x, 0 ); viewportTL.x = Math.max( viewportTL.x, 0 );
viewportBR.x = Math.min( viewportBR.x, 1 ); viewportBR.x = Math.min( viewportBR.x, 1 );
@ -480,10 +408,12 @@ function updateViewport( drawer ) {
viewportBR.y = Math.min( viewportBR.y, drawer.normHeight ); viewportBR.y = Math.min( viewportBR.y, drawer.normHeight );
} }
//TODO // Calculations for the interval of levels to draw
// (above in initial var statement)
// can return invalid intervals; fix that here if necessary
lowestLevel = Math.min( lowestLevel, highestLevel ); lowestLevel = Math.min( lowestLevel, highestLevel );
//TODO // Update any level that will be drawn
var drawLevel; // FIXME: drawLevel should have a more explanatory name var drawLevel; // FIXME: drawLevel should have a more explanatory name
for ( level = highestLevel; level >= lowestLevel; level-- ) { for ( level = highestLevel; level >= lowestLevel; level-- ) {
drawLevel = false; drawLevel = false;
@ -528,7 +458,7 @@ function updateViewport( drawer ) {
optimalRatio - renderPixelRatioT optimalRatio - renderPixelRatioT
); );
//TODO // Update the level and keep track of 'best' tile to load
best = updateLevel( best = updateLevel(
drawer, drawer,
haveDrawn, haveDrawn,
@ -542,16 +472,17 @@ function updateViewport( drawer ) {
best best
); );
//TODO // Stop the loop if lower-res tiles would all be covered by
// already drawn tiles
if ( providesCoverage( drawer.coverage, level ) ) { if ( providesCoverage( drawer.coverage, level ) ) {
break; break;
} }
} }
//TODO // Perform the actual drawing
drawTiles( drawer, drawer.lastDrawn ); drawTiles( drawer, drawer.lastDrawn );
//TODO // Load the new 'best' tile
if ( best ) { if ( best ) {
loadTile( drawer, best, currentTime ); loadTile( drawer, best, currentTime );
// because we haven't finished drawing, so // because we haven't finished drawing, so
@ -756,18 +687,18 @@ function getTile( x, y, level, tileSource, tilesMatrix, time, numTiles, normHeig
return tile; return tile;
} }
function loadTile( drawer, tile, time ) { function loadTile( drawer, tile, time ) {
if( drawer.viewport.collectionMode ){ if( drawer.viewport.collectionMode ){
drawer.midUpdate = false; drawer.midUpdate = false;
onTileLoad( drawer, tile, time ); onTileLoad( drawer, tile, time );
} else { } else {
tile.loading = drawer.loadImage( tile.loading = true;
tile.url, drawer.imageLoader.addJob({
function( image ){ src: tile.url,
callback: function( image ){
onTileLoad( drawer, tile, time, image ); onTileLoad( drawer, tile, time, image );
} }
); });
} }
} }
@ -1018,21 +949,6 @@ function compareTiles( previousBest, tile ) {
return previousBest; return previousBest;
} }
function finishLoadingImage( image, callback, successful, jobid ){
image.onload = null;
image.onabort = null;
image.onerror = null;
if ( jobid ) {
window.clearTimeout( jobid );
}
$.requestAnimationFrame( function() {
callback( image.src, successful ? image : null);
});
}
function drawTiles( drawer, lastDrawn ){ function drawTiles( drawer, lastDrawn ){
var i, var i,
tile, tile,

171
src/imageLoader.js Normal file
View File

@ -0,0 +1,171 @@
/*
* OpenSeadragon - ImageLoader
*
* Copyright (C) 2009 CodePlex Foundation
* Copyright (C) 2010-2013 OpenSeadragon contributors
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* - Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* - Neither the name of CodePlex Foundation nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
* TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
(function( $ ){
/**
* @private
* @class ImageJob
* @classdesc Handles loading a single image for use in a single {@link OpenSeadragon.Tile}.
*
* @memberof OpenSeadragon
* @param {String} source - URL of image to download.
* @param {Function} callback - Called once image has finished downloading.
*/
function ImageJob ( options ) {
$.extend( true, this, {
timeout: $.DEFAULT_SETTINGS.timeout,
jobId: null,
}, options );
/**
* Image object which will contain downloaded image.
* @member {Image} image
* @memberof OpenSeadragon.ImageJob#
*/
this.image = null;
}
ImageJob.prototype = {
/**
* Initiates downloading of associated image.
* @method
*/
start: function(){
var _this = this;
this.image = new Image();
if ( _this.crossOriginPolicy !== false ) {
this.image.crossOrigin = this.crossOriginPolicy;
}
this.image.onload = function(){
_this.finish( true );
};
this.image.onabort = this.image.onerror = function(){
_this.finish( false );
};
this.jobId = window.setTimeout( function(){
_this.finish( false );
}, this.timeout);
this.image.src = this.src;
},
finish: function( successful ) {
this.image.onload = this.image.onerror = this.image.onabort = null;
if (!successful) {
this.image = null;
}
if ( this.jobId ) {
window.clearTimeout( this.jobId );
}
this.callback( this );
}
};
/**
* @class
* @classdesc Handles downloading of a set of images using asynchronous queue pattern.
*/
$.ImageLoader = function() {
$.extend( true, this, {
jobLimit: $.DEFAULT_SETTINGS.imageLoaderLimit,
jobQueue: [],
jobsInProgress: 0
});
};
$.ImageLoader.prototype = {
/**
* Add an unloaded image to the loader queue.
* @method
* @param {String} src - URL of image to download.
* @param {Function} callback - Called once image has been downloaded.
*/
addJob: function( options ) {
var _this = this,
complete = function( job ) {
completeJob( _this, job, options.callback );
},
jobOptions = {
src: options.src,
callback: complete
},
newJob = new ImageJob( jobOptions );
if ( !this.jobLimit || this.jobsInProgress < this.jobLimit ) {
newJob.start();
this.jobsInProgress++;
}
else {
this.jobQueue.push( newJob );
}
}
};
/**
* Cleans up ImageJob once completed.
* @method
* @private
* @param loader - ImageLoader used to start job.
* @param job - The ImageJob that has completed.
* @param callback - Called once cleanup is finished.
*/
function completeJob( loader, job, callback ) {
var nextJob;
loader.jobsInProgress--;
if ( (!loader.jobLimit || loader.jobsInProgress < loader.jobLimit) && loader.jobQueue.length > 0) {
nextJob = loader.jobQueue.shift();
nextJob.start();
}
callback( job.image );
}
}( OpenSeadragon ));