From dd51df97ab9946555f86ab3a9449deb35d1aef95 Mon Sep 17 00:00:00 2001 From: Ryan Lester Date: Sun, 4 May 2014 19:11:50 -0700 Subject: [PATCH] Added ImageLoader; loads batches of images using async queue pattern --- Gruntfile.js | 23 +++--- src/drawer.js | 120 +++++-------------------------- src/imageLoader.js | 171 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 201 insertions(+), 113 deletions(-) create mode 100644 src/imageLoader.js diff --git a/Gruntfile.js b/Gruntfile.js index d3036d14..a90a0974 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -45,6 +45,7 @@ module.exports = function(grunt) { "src/referencestrip.js", "src/displayrectangle.js", "src/spring.js", + "src/imageLoader.js", "src/tile.js", "src/overlay.js", "src/drawer.js", @@ -73,11 +74,11 @@ module.exports = function(grunt) { }, concat: { options: { - banner: "//! <%= pkg.name %> <%= pkg.version %>\n" - + "//! Built on <%= grunt.template.today('yyyy-mm-dd') %>\n" - + "//! Git commit: <%= gitInfo %>\n" - + "//! http://openseadragon.github.io\n" - + "//! License: http://openseadragon.github.io/license/\n\n", + banner: "//! <%= pkg.name %> <%= pkg.version %>\n" + + "//! Built on <%= grunt.template.today('yyyy-mm-dd') %>\n" + + "//! Git commit: <%= gitInfo %>\n" + + "//! http://openseadragon.github.io\n" + + "//! License: http://openseadragon.github.io/license/\n\n", process: true }, dist: { @@ -182,9 +183,9 @@ module.exports = function(grunt) { // Creates a directory tree to be compressed into a package. grunt.registerTask("copy:package", function() { grunt.file.recurse("build/openseadragon", function(abspath, rootdir, subdir, filename) { - var dest = packageDir - + (subdir ? subdir + "/" : '/') - + filename; + var dest = packageDir + + (subdir ? subdir + "/" : '/') + + filename; grunt.file.copy(abspath, dest); }); grunt.file.copy("changelog.txt", packageDir + "changelog.txt"); @@ -200,9 +201,9 @@ module.exports = function(grunt) { return; } - var dest = releaseRoot - + (subdir ? subdir + "/" : '/') - + filename; + var dest = releaseRoot + + (subdir ? subdir + "/" : '/') + + filename; grunt.file.copy(abspath, dest); }); diff --git a/src/drawer.js b/src/drawer.js index 1c7ad229..54b07281 100644 --- a/src/drawer.js +++ b/src/drawer.js @@ -76,7 +76,7 @@ $.Drawer = function( options ) { //internal state properties viewer: null, - downloading: 0, // How many images are currently being loaded in parallel. + imageLoader: new $.ImageLoader(), tilesMatrix: {}, // A '3d' dictionary [level][x][y] --> Tile. tilesLoaded: [], // An unordered list of Tiles with loaded images. coverage: {}, // A '3d' dictionary [level][x][y] --> Boolean. @@ -92,7 +92,6 @@ $.Drawer = function( options ) { //configurable settings opacity: $.DEFAULT_SETTINGS.opacity, maxImageCacheCount: $.DEFAULT_SETTINGS.maxImageCacheCount, - imageLoaderLimit: $.DEFAULT_SETTINGS.imageLoaderLimit, minZoomImageRatio: $.DEFAULT_SETTINGS.minZoomImageRatio, wrapHorizontal: $.DEFAULT_SETTINGS.wrapHorizontal, wrapVertical: $.DEFAULT_SETTINGS.wrapVertical, @@ -296,77 +295,6 @@ $.Drawer.prototype = /** @lends OpenSeadragon.Drawer.prototype */{ 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. * @method @@ -436,13 +364,13 @@ function updateViewport( drawer ) { levelOpacity, levelVisibility; - //TODO + // Reset tile's internal drawn state while ( drawer.lastDrawn.length > 0 ) { tile = drawer.lastDrawn.pop(); tile.beingDrawn = false; } - //TODO + // Clear canvas drawer.canvas.innerHTML = ""; if ( drawer.useCanvas ) { if( drawer.canvas.width != viewportSize.x || @@ -470,7 +398,7 @@ function updateViewport( drawer ) { return; } - //TODO + // Calculate viewport rect / bounds if ( !drawer.wrapHorizontal ) { viewportTL.x = Math.max( viewportTL.x, 0 ); viewportBR.x = Math.min( viewportBR.x, 1 ); @@ -480,10 +408,12 @@ function updateViewport( drawer ) { 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 ); - //TODO + // Update any level that will be drawn var drawLevel; // FIXME: drawLevel should have a more explanatory name for ( level = highestLevel; level >= lowestLevel; level-- ) { drawLevel = false; @@ -528,7 +458,7 @@ function updateViewport( drawer ) { optimalRatio - renderPixelRatioT ); - //TODO + // Update the level and keep track of 'best' tile to load best = updateLevel( drawer, haveDrawn, @@ -542,16 +472,17 @@ function updateViewport( drawer ) { best ); - //TODO + // Stop the loop if lower-res tiles would all be covered by + // already drawn tiles if ( providesCoverage( drawer.coverage, level ) ) { break; } } - //TODO + // Perform the actual drawing drawTiles( drawer, drawer.lastDrawn ); - //TODO + // Load the new 'best' tile if ( best ) { loadTile( drawer, best, currentTime ); // because we haven't finished drawing, so @@ -756,18 +687,18 @@ function getTile( x, y, level, tileSource, tilesMatrix, time, numTiles, normHeig return tile; } - function loadTile( drawer, tile, time ) { if( drawer.viewport.collectionMode ){ drawer.midUpdate = false; onTileLoad( drawer, tile, time ); } else { - tile.loading = drawer.loadImage( - tile.url, - function( image ){ + tile.loading = true; + drawer.imageLoader.addJob({ + src: tile.url, + callback: function( image ){ onTileLoad( drawer, tile, time, image ); } - ); + }); } } @@ -1018,21 +949,6 @@ function compareTiles( previousBest, tile ) { 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 ){ var i, tile, diff --git a/src/imageLoader.js b/src/imageLoader.js new file mode 100644 index 00000000..2e810d38 --- /dev/null +++ b/src/imageLoader.js @@ -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 )); +