diff --git a/src/tiledimage.js b/src/tiledimage.js
new file mode 100644
index 00000000..a4b7f2ec
--- /dev/null
+++ b/src/tiledimage.js
@@ -0,0 +1,1237 @@
+/*
+ * OpenSeadragon - Drawer
+ *
+ * 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( $ ){
+
+var DEVICE_SCREEN = $.getWindowSize(),
+ BROWSER = $.Browser.vendor,
+ BROWSER_VERSION = $.Browser.version,
+
+ SUBPIXEL_RENDERING = (
+ ( BROWSER == $.BROWSERS.FIREFOX ) ||
+ ( BROWSER == $.BROWSERS.OPERA ) ||
+ ( BROWSER == $.BROWSERS.SAFARI && BROWSER_VERSION >= 4 ) ||
+ ( BROWSER == $.BROWSERS.CHROME && BROWSER_VERSION >= 2 ) ||
+ ( BROWSER == $.BROWSERS.IE && BROWSER_VERSION >= 9 )
+ );
+
+
+/**
+ * @class Drawer
+ * @classdesc Handles rendering of tiles for an {@link OpenSeadragon.Viewer}.
+ * A new instance is created for each TileSource opened (see {@link OpenSeadragon.Viewer#drawer}).
+ *
+ * @memberof OpenSeadragon
+ * @param {OpenSeadragon.TileSource} source - Reference to Viewer tile source.
+ * @param {OpenSeadragon.Viewport} viewport - Reference to Viewer viewport.
+ * @param {Element} element - Parent element.
+ */
+$.Drawer = function( options ) {
+
+ //backward compatibility for positional args while prefering more
+ //idiomatic javascript options object as the only argument
+ var args = arguments,
+ i;
+
+ if( !$.isPlainObject( options ) ){
+ options = {
+ source: args[ 0 ], // Reference to Viewer tile source.
+ viewport: args[ 1 ], // Reference to Viewer viewport.
+ element: args[ 2 ] // Parent element.
+ };
+ }
+
+ this._worldX = options.x || 0;
+ delete options.x;
+ this._worldY = options.y || 0;
+ delete options.y;
+
+ // Ratio of zoomable image height to width.
+ this.normHeight = options.source.dimensions.y / options.source.dimensions.x;
+
+ if ( options.width ) {
+ this._scale = options.width;
+ delete options.width;
+
+ if ( options.height ) {
+ $.console.error( "specifying both width and height to a drawer is not supported" );
+ delete options.height;
+ }
+ } else if ( options.height ) {
+ this._scale = options.height / this.normHeight;
+ delete options.height;
+ } else {
+ this._scale = 1;
+ }
+
+ this._worldWidth = this._scale;
+ this._worldHeight = this.normHeight * this._scale;
+
+ $.extend( true, this, {
+
+ //internal state properties
+ viewer: null,
+ 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.
+ lastDrawn: [], // An unordered list of Tiles drawn last frame.
+ lastResetTime: 0, // Last time for which the drawer was reset.
+ midUpdate: false, // Is the drawer currently updating the viewport?
+ updateAgain: true, // Does the drawer need to update the viewport again?
+
+
+ //internal state / configurable settings
+ collectionOverlays: {}, // For collection mode. Here an overlay is actually a viewer.
+
+ //configurable settings
+ opacity: $.DEFAULT_SETTINGS.opacity,
+ maxImageCacheCount: $.DEFAULT_SETTINGS.maxImageCacheCount,
+ minZoomImageRatio: $.DEFAULT_SETTINGS.minZoomImageRatio,
+ wrapHorizontal: $.DEFAULT_SETTINGS.wrapHorizontal,
+ wrapVertical: $.DEFAULT_SETTINGS.wrapVertical,
+ immediateRender: $.DEFAULT_SETTINGS.immediateRender,
+ blendTime: $.DEFAULT_SETTINGS.blendTime,
+ alwaysBlend: $.DEFAULT_SETTINGS.alwaysBlend,
+ minPixelRatio: $.DEFAULT_SETTINGS.minPixelRatio,
+ debugMode: $.DEFAULT_SETTINGS.debugMode,
+ timeout: $.DEFAULT_SETTINGS.timeout,
+ crossOriginPolicy: $.DEFAULT_SETTINGS.crossOriginPolicy
+
+ }, options );
+
+ this.useCanvas = $.supportsCanvas && ( this.viewer ? this.viewer.useCanvas : true );
+ /**
+ * The parent element of this Drawer instance, passed in when the Drawer was created.
+ * The parent of {@link OpenSeadragon.Drawer#canvas}.
+ * @member {Element} container
+ * @memberof OpenSeadragon.Drawer#
+ */
+ this.container = $.getElement( this.element );
+ /**
+ * A <canvas> element if the browser supports them, otherwise a <div> element.
+ * Child element of {@link OpenSeadragon.Drawer#container}.
+ * @member {Element} canvas
+ * @memberof OpenSeadragon.Drawer#
+ */
+ this.canvas = $.makeNeutralElement( this.useCanvas ? "canvas" : "div" );
+ /**
+ * 2d drawing context for {@link OpenSeadragon.Drawer#canvas} if it's a <canvas> element, otherwise null.
+ * @member {Object} context
+ * @memberof OpenSeadragon.Drawer#
+ */
+ this.context = this.useCanvas ? this.canvas.getContext( "2d" ) : null;
+
+ /**
+ * @member {Element} element
+ * @memberof OpenSeadragon.Drawer#
+ * @deprecated Alias for {@link OpenSeadragon.Drawer#container}.
+ */
+ this.element = this.container;
+
+ // We force our container to ltr because our drawing math doesn't work in rtl.
+ // This issue only affects our canvas renderer, but we do it always for consistency.
+ // Note that this means overlays you want to be rtl need to be explicitly set to rtl.
+ this.container.dir = 'ltr';
+
+ this.canvas.style.width = "100%";
+ this.canvas.style.height = "100%";
+ this.canvas.style.position = "absolute";
+ $.setElementOpacity( this.canvas, this.opacity, true );
+
+ // explicit left-align
+ this.container.style.textAlign = "left";
+ this.container.appendChild( this.canvas );
+
+ //this.profiler = new $.Profiler();
+};
+
+$.Drawer.prototype = /** @lends OpenSeadragon.Drawer.prototype */{
+
+ /**
+ * Adds an html element as an overlay to the current viewport. Useful for
+ * highlighting words or areas of interest on an image or other zoomable
+ * interface.
+ * @method
+ * @param {Element|String|Object} element - A reference to an element or an id for
+ * the element which will overlayed. Or an Object specifying the configuration for the overlay
+ * @param {OpenSeadragon.Point|OpenSeadragon.Rect} location - The point or
+ * rectangle which will be overlayed.
+ * @param {OpenSeadragon.OverlayPlacement} placement - The position of the
+ * viewport which the location coordinates will be treated as relative
+ * to.
+ * @param {function} onDraw - If supplied the callback is called when the overlay
+ * needs to be drawn. It it the responsibility of the callback to do any drawing/positioning.
+ * It is passed position, size and element.
+ * @fires OpenSeadragon.Viewer.event:add-overlay
+ * @deprecated - use {@link OpenSeadragon.Viewer#addOverlay} instead.
+ */
+ addOverlay: function( element, location, placement, onDraw ) {
+ $.console.error("drawer.addOverlay is deprecated. Use viewer.addOverlay instead.");
+ this.viewer.addOverlay( element, location, placement, onDraw );
+ return this;
+ },
+
+ /**
+ * Updates the overlay represented by the reference to the element or
+ * element id moving it to the new location, relative to the new placement.
+ * @method
+ * @param {OpenSeadragon.Point|OpenSeadragon.Rect} location - The point or
+ * rectangle which will be overlayed.
+ * @param {OpenSeadragon.OverlayPlacement} placement - The position of the
+ * viewport which the location coordinates will be treated as relative
+ * to.
+ * @return {OpenSeadragon.Drawer} Chainable.
+ * @fires OpenSeadragon.Viewer.event:update-overlay
+ * @deprecated - use {@link OpenSeadragon.Viewer#updateOverlay} instead.
+ */
+ updateOverlay: function( element, location, placement ) {
+ $.console.error("drawer.updateOverlay is deprecated. Use viewer.updateOverlay instead.");
+ this.viewer.updateOverlay( element, location, placement );
+ return this;
+ },
+
+ /**
+ * Removes and overlay identified by the reference element or element id
+ * and schedules and update.
+ * @method
+ * @param {Element|String} element - A reference to the element or an
+ * element id which represent the ovelay content to be removed.
+ * @return {OpenSeadragon.Drawer} Chainable.
+ * @fires OpenSeadragon.Viewer.event:remove-overlay
+ * @deprecated - use {@link OpenSeadragon.Viewer#removeOverlay} instead.
+ */
+ removeOverlay: function( element ) {
+ $.console.error("drawer.removeOverlay is deprecated. Use viewer.removeOverlay instead.");
+ this.viewer.removeOverlay( element );
+ return this;
+ },
+
+ /**
+ * Removes all currently configured Overlays from this Drawer and schedules
+ * and update.
+ * @method
+ * @return {OpenSeadragon.Drawer} Chainable.
+ * @fires OpenSeadragon.Viewer.event:clear-overlay
+ * @deprecated - use {@link OpenSeadragon.Viewer#clearOverlays} instead.
+ */
+ clearOverlays: function() {
+ $.console.error("drawer.clearOverlays is deprecated. Use viewer.clearOverlays instead.");
+ this.viewer.clearOverlays();
+ return this;
+ },
+
+ /**
+ * Set the opacity of the drawer.
+ * @method
+ * @param {Number} opacity
+ * @return {OpenSeadragon.Drawer} Chainable.
+ */
+ setOpacity: function( opacity ) {
+ this.opacity = opacity;
+ $.setElementOpacity( this.canvas, this.opacity, true );
+ return this;
+ },
+
+ /**
+ * Get the opacity of the drawer.
+ * @method
+ * @returns {Number}
+ */
+ getOpacity: function() {
+ return this.opacity;
+ },
+ /**
+ * Returns whether the Drawer is scheduled for an update at the
+ * soonest possible opportunity.
+ * @method
+ * @returns {Boolean} - Whether the Drawer is scheduled for an update at the
+ * soonest possible opportunity.
+ */
+ needsUpdate: function() {
+ return this.updateAgain;
+ },
+
+ /**
+ * Returns the total number of tiles that have been loaded by this Drawer.
+ * @method
+ * @returns {Number} - The total number of tiles that have been loaded by
+ * this Drawer.
+ */
+ numTilesLoaded: function() {
+ return this.tilesLoaded.length;
+ },
+
+ /**
+ * Clears all tiles and triggers an update on the next call to
+ * Drawer.prototype.update().
+ * @method
+ * @return {OpenSeadragon.Drawer} Chainable.
+ */
+ reset: function() {
+ clearTiles( this );
+ this.lastResetTime = $.now();
+ this.updateAgain = true;
+ return this;
+ },
+
+ /**
+ * Forces the Drawer to update.
+ * @method
+ * @return {OpenSeadragon.Drawer} Chainable.
+ */
+ update: function() {
+ //this.profiler.beginUpdate();
+ this.midUpdate = true;
+ updateViewport( this );
+ this.midUpdate = false;
+ //this.profiler.endUpdate();
+ return this;
+ },
+
+ /**
+ * Returns whether rotation is supported or not.
+ * @method
+ * @return {Boolean} True if rotation is supported.
+ */
+ canRotate: function() {
+ return this.useCanvas;
+ },
+
+ /**
+ * Destroy the drawer (unload current loaded tiles)
+ * @method
+ * @return null
+ */
+ destroy: function() {
+ //unload current loaded tiles (=empty TILE_CACHE)
+ for ( var i = 0; i < this.tilesLoaded.length; ++i ) {
+ this.tilesLoaded[i].unload();
+ }
+
+ //force unloading of current canvas (1x1 will be gc later, trick not necessarily needed)
+ this.canvas.width = 1;
+ this.canvas.height = 1;
+ }
+};
+
+/**
+ * @private
+ * @inner
+ * Pretty much every other line in this needs to be documented so it's clear
+ * how each piece of this routine contributes to the drawing process. That's
+ * why there are so many TODO's inside this function.
+ */
+function updateViewport( drawer ) {
+
+ drawer.updateAgain = false;
+
+ if( drawer.viewer ){
+ /**
+ * - Needs documentation -
+ *
+ * @event update-viewport
+ * @memberof OpenSeadragon.Viewer
+ * @type {object}
+ * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
+ * @property {?Object} userData - Arbitrary subscriber-defined object.
+ */
+ drawer.viewer.raiseEvent( 'update-viewport', {} );
+ }
+
+ var tile,
+ level,
+ best = null,
+ haveDrawn = false,
+ currentTime = $.now(),
+ viewportSize = drawer.viewport.getContainerSize(),
+ viewportBounds = drawer.viewport.getBounds( true ),
+ viewportTL = viewportBounds.getTopLeft(),
+ viewportBR = viewportBounds.getBottomRight(),
+ zeroRatioC = drawer.viewport.deltaPixelsFromPoints(
+ drawer.source.getPixelRatio( 0 ),
+ true
+ ).x * drawer._scale,
+ lowestLevel = Math.max(
+ drawer.source.minLevel,
+ Math.floor(
+ Math.log( drawer.minZoomImageRatio ) /
+ Math.log( 2 )
+ )
+ ),
+ highestLevel = Math.min(
+ Math.abs(drawer.source.maxLevel),
+ Math.abs(Math.floor(
+ Math.log( zeroRatioC / drawer.minPixelRatio ) /
+ Math.log( 2 )
+ ))
+ ),
+ degrees = drawer.viewport.degrees,
+ renderPixelRatioC,
+ renderPixelRatioT,
+ zeroRatioT,
+ optimalRatio,
+ levelOpacity,
+ levelVisibility;
+
+ viewportTL.x -= drawer._worldX;
+ viewportTL.y -= drawer._worldY;
+ viewportBR.x -= drawer._worldX;
+ viewportBR.y -= drawer._worldY;
+
+ // Reset tile's internal drawn state
+ while ( drawer.lastDrawn.length > 0 ) {
+ tile = drawer.lastDrawn.pop();
+ tile.beingDrawn = false;
+ }
+
+ // Clear canvas
+ drawer.canvas.innerHTML = "";
+ if ( drawer.useCanvas ) {
+ if( drawer.canvas.width != viewportSize.x ||
+ drawer.canvas.height != viewportSize.y ){
+ drawer.canvas.width = viewportSize.x;
+ drawer.canvas.height = viewportSize.y;
+ }
+ drawer.context.clearRect( 0, 0, viewportSize.x, viewportSize.y );
+ }
+
+ //Change bounds for rotation
+ if (degrees === 90 || degrees === 270) {
+ var rotatedBounds = viewportBounds.rotate( degrees );
+ viewportTL = rotatedBounds.getTopLeft();
+ viewportBR = rotatedBounds.getBottomRight();
+ }
+
+ //Don't draw if completely outside of the viewport
+ if ( !drawer.wrapHorizontal &&
+ ( viewportBR.x < 0 || viewportTL.x > drawer._worldWidth ) ) {
+ return;
+ } else if
+ ( !drawer.wrapVertical &&
+ ( viewportBR.y < 0 || viewportTL.y > drawer._worldHeight ) ) {
+ return;
+ }
+
+ // Calculate viewport rect / bounds
+ if ( !drawer.wrapHorizontal ) {
+ viewportTL.x = Math.max( viewportTL.x, 0 );
+ viewportBR.x = Math.min( viewportBR.x, drawer._worldWidth );
+ }
+ if ( !drawer.wrapVertical ) {
+ viewportTL.y = Math.max( viewportTL.y, 0 );
+ viewportBR.y = Math.min( viewportBR.y, drawer._worldHeight );
+ }
+
+ // 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 );
+
+ // 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;
+
+ //Avoid calculations for draw if we have already drawn this
+ renderPixelRatioC = drawer.viewport.deltaPixelsFromPoints(
+ drawer.source.getPixelRatio( level ),
+ true
+ ).x * drawer._scale;
+
+ if ( ( !haveDrawn && renderPixelRatioC >= drawer.minPixelRatio ) ||
+ ( level == lowestLevel ) ) {
+ drawLevel = true;
+ haveDrawn = true;
+ } else if ( !haveDrawn ) {
+ continue;
+ }
+
+ //Perform calculations for draw if we haven't drawn this
+ renderPixelRatioT = drawer.viewport.deltaPixelsFromPoints(
+ drawer.source.getPixelRatio( level ),
+ false
+ ).x * drawer._scale;
+
+ zeroRatioT = drawer.viewport.deltaPixelsFromPoints(
+ drawer.source.getPixelRatio(
+ Math.max(
+ drawer.source.getClosestLevel( drawer.viewport.containerSize ) - 1,
+ 0
+ )
+ ),
+ false
+ ).x * drawer._scale;
+
+ optimalRatio = drawer.immediateRender ?
+ 1 :
+ zeroRatioT;
+
+ levelOpacity = Math.min( 1, ( renderPixelRatioC - 0.5 ) / 0.5 );
+
+ levelVisibility = optimalRatio / Math.abs(
+ optimalRatio - renderPixelRatioT
+ );
+
+ // Update the level and keep track of 'best' tile to load
+ best = updateLevel(
+ drawer,
+ haveDrawn,
+ drawLevel,
+ level,
+ levelOpacity,
+ levelVisibility,
+ viewportTL,
+ viewportBR,
+ currentTime,
+ best
+ );
+
+ // Stop the loop if lower-res tiles would all be covered by
+ // already drawn tiles
+ if ( providesCoverage( drawer.coverage, level ) ) {
+ break;
+ }
+ }
+
+ // Perform the actual drawing
+ drawTiles( drawer, drawer.lastDrawn );
+
+ // Load the new 'best' tile
+ if ( best ) {
+ loadTile( drawer, best, currentTime );
+ // because we haven't finished drawing, so
+ drawer.updateAgain = true;
+ }
+
+}
+
+
+function updateLevel( drawer, haveDrawn, drawLevel, level, levelOpacity, levelVisibility, viewportTL, viewportBR, currentTime, best ){
+
+ var x, y,
+ tileTL,
+ tileBR,
+ numberOfTiles,
+ viewportCenter = drawer.viewport.pixelFromPoint( drawer.viewport.getCenter() );
+
+
+ if( drawer.viewer ){
+ /**
+ * - Needs documentation -
+ *
+ * @event update-level
+ * @memberof OpenSeadragon.Viewer
+ * @type {object}
+ * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
+ * @property {Object} havedrawn
+ * @property {Object} level
+ * @property {Object} opacity
+ * @property {Object} visibility
+ * @property {Object} topleft
+ * @property {Object} bottomright
+ * @property {Object} currenttime
+ * @property {Object} best
+ * @property {?Object} userData - Arbitrary subscriber-defined object.
+ */
+ drawer.viewer.raiseEvent( 'update-level', {
+ havedrawn: haveDrawn,
+ level: level,
+ opacity: levelOpacity,
+ visibility: levelVisibility,
+ topleft: viewportTL,
+ bottomright: viewportBR,
+ currenttime: currentTime,
+ best: best
+ });
+ }
+
+ //OK, a new drawing so do your calculations
+ tileTL = drawer.source.getTileAtPoint( level, viewportTL.divide( drawer._scale ));
+ tileBR = drawer.source.getTileAtPoint( level, viewportBR.divide( drawer._scale ));
+ numberOfTiles = drawer.source.getNumTiles( level );
+
+ resetCoverage( drawer.coverage, level );
+
+ if ( !drawer.wrapHorizontal ) {
+ tileBR.x = Math.min( tileBR.x, numberOfTiles.x - 1 );
+ }
+ if ( !drawer.wrapVertical ) {
+ tileBR.y = Math.min( tileBR.y, numberOfTiles.y - 1 );
+ }
+
+ for ( x = tileTL.x; x <= tileBR.x; x++ ) {
+ for ( y = tileTL.y; y <= tileBR.y; y++ ) {
+
+ best = updateTile(
+ drawer,
+ drawLevel,
+ haveDrawn,
+ x, y,
+ level,
+ levelOpacity,
+ levelVisibility,
+ viewportCenter,
+ numberOfTiles,
+ currentTime,
+ best
+ );
+
+ }
+ }
+
+ return best;
+}
+
+function updateTile( drawer, drawLevel, haveDrawn, x, y, level, levelOpacity, levelVisibility, viewportCenter, numberOfTiles, currentTime, best){
+
+ var tile = getTile(
+ x, y,
+ level,
+ drawer.source,
+ drawer.tilesMatrix,
+ currentTime,
+ numberOfTiles,
+ drawer._worldWidth,
+ drawer._worldHeight
+ ),
+ drawTile = drawLevel;
+
+ if( drawer.viewer ){
+ /**
+ * - Needs documentation -
+ *
+ * @event update-tile
+ * @memberof OpenSeadragon.Viewer
+ * @type {object}
+ * @property {OpenSeadragon.Viewer} eventSource - A reference to the Viewer which raised the event.
+ * @property {OpenSeadragon.Tile} tile
+ * @property {?Object} userData - Arbitrary subscriber-defined object.
+ */
+ drawer.viewer.raiseEvent( 'update-tile', {
+ tile: tile
+ });
+ }
+
+ setCoverage( drawer.coverage, level, x, y, false );
+
+ if ( !tile.exists ) {
+ return best;
+ }
+
+ if ( haveDrawn && !drawTile ) {
+ if ( isCovered( drawer.coverage, level, x, y ) ) {
+ setCoverage( drawer.coverage, level, x, y, true );
+ } else {
+ drawTile = true;
+ }
+ }
+
+ if ( !drawTile ) {
+ return best;
+ }
+
+ positionTile(
+ tile,
+ drawer.source.tileOverlap,
+ drawer.viewport,
+ viewportCenter,
+ levelVisibility,
+ drawer
+ );
+
+ if ( tile.loaded ) {
+ var needsUpdate = blendTile(
+ drawer,
+ tile,
+ x, y,
+ level,
+ levelOpacity,
+ currentTime
+ );
+
+ if ( needsUpdate ) {
+ drawer.updateAgain = true;
+ }
+ } else if ( tile.loading ) {
+ // the tile is already in the download queue
+ // thanks josh1093 for finally translating this typo
+ } else {
+ best = compareTiles( best, tile );
+ }
+
+ return best;
+}
+
+function getTile( x, y, level, tileSource, tilesMatrix, time, numTiles, worldWidth, worldHeight ) {
+ var xMod,
+ yMod,
+ bounds,
+ exists,
+ url,
+ tile;
+
+ if ( !tilesMatrix[ level ] ) {
+ tilesMatrix[ level ] = {};
+ }
+ if ( !tilesMatrix[ level ][ x ] ) {
+ tilesMatrix[ level ][ x ] = {};
+ }
+
+ if ( !tilesMatrix[ level ][ x ][ y ] ) {
+ xMod = ( numTiles.x + ( x % numTiles.x ) ) % numTiles.x;
+ yMod = ( numTiles.y + ( y % numTiles.y ) ) % numTiles.y;
+ bounds = tileSource.getTileBounds( level, xMod, yMod );
+ exists = tileSource.tileExists( level, xMod, yMod );
+ url = tileSource.getTileUrl( level, xMod, yMod );
+
+ bounds.x += worldWidth * ( x - xMod ) / numTiles.x;
+ bounds.y += worldHeight * ( y - yMod ) / numTiles.y;
+
+ tilesMatrix[ level ][ x ][ y ] = new $.Tile(
+ level,
+ x,
+ y,
+ bounds,
+ exists,
+ url
+ );
+ }
+
+ tile = tilesMatrix[ level ][ x ][ y ];
+ tile.lastTouchTime = time;
+
+ return tile;
+}
+
+function loadTile( drawer, tile, time ) {
+ if( drawer.viewport.collectionMode ){
+ drawer.midUpdate = false;
+ onTileLoad( drawer, tile, time );
+ } else {
+ tile.loading = true;
+ drawer.imageLoader.addJob({
+ src: tile.url,
+ crossOriginPolicy: drawer.crossOriginPolicy,
+ callback: function( image ){
+ onTileLoad( drawer, tile, time, image );
+ }
+ });
+ }
+}
+
+function onTileLoad( drawer, tile, time, image ) {
+ var insertionIndex,
+ cutoff,
+ worstTile,
+ worstTime,
+ worstLevel,
+ worstTileIndex,
+ prevTile,
+ prevTime,
+ prevLevel,
+ i;
+
+ tile.loading = false;
+
+ if ( drawer.midUpdate ) {
+ $.console.warn( "Tile load callback in middle of drawing routine." );
+ return;
+ } else if ( !image && !drawer.viewport.collectionMode ) {
+ $.console.log( "Tile %s failed to load: %s", tile, tile.url );
+ if( !drawer.debugMode ){
+ tile.exists = false;
+ return;
+ }
+ } else if ( time < drawer.lastResetTime ) {
+ $.console.log( "Ignoring tile %s loaded before reset: %s", tile, tile.url );
+ return;
+ }
+
+ tile.loaded = true;
+ tile.image = image;
+
+ insertionIndex = drawer.tilesLoaded.length;
+
+ if ( drawer.tilesLoaded.length >= drawer.maxImageCacheCount ) {
+ cutoff = Math.ceil( Math.log( drawer.source.getTileSize(tile.level) ) / Math.log( 2 ) );
+
+ worstTile = null;
+ worstTileIndex = -1;
+
+ for ( i = drawer.tilesLoaded.length - 1; i >= 0; i-- ) {
+ prevTile = drawer.tilesLoaded[ i ];
+
+ if ( prevTile.level <= drawer.cutoff || prevTile.beingDrawn ) {
+ continue;
+ } else if ( !worstTile ) {
+ worstTile = prevTile;
+ worstTileIndex = i;
+ continue;
+ }
+
+ prevTime = prevTile.lastTouchTime;
+ worstTime = worstTile.lastTouchTime;
+ prevLevel = prevTile.level;
+ worstLevel = worstTile.level;
+
+ if ( prevTime < worstTime ||
+ ( prevTime == worstTime && prevLevel > worstLevel ) ) {
+ worstTile = prevTile;
+ worstTileIndex = i;
+ }
+ }
+
+ if ( worstTile && worstTileIndex >= 0 ) {
+ worstTile.unload();
+ insertionIndex = worstTileIndex;
+ }
+ }
+
+ drawer.tilesLoaded[ insertionIndex ] = tile;
+ drawer.updateAgain = true;
+}
+
+
+function positionTile( tile, overlap, viewport, viewportCenter, levelVisibility, drawer ){
+ var boundsTL = tile.bounds.getTopLeft();
+
+ boundsTL.x *= drawer._scale;
+ boundsTL.y *= drawer._scale;
+ boundsTL.x += drawer._worldX;
+ boundsTL.y += drawer._worldY;
+
+ var boundsSize = tile.bounds.getSize();
+
+ boundsSize.x *= drawer._scale;
+ boundsSize.y *= drawer._scale;
+
+ var positionC = viewport.pixelFromPoint( boundsTL, true ),
+ positionT = viewport.pixelFromPoint( boundsTL, false ),
+ sizeC = viewport.deltaPixelsFromPoints( boundsSize, true ),
+ sizeT = viewport.deltaPixelsFromPoints( boundsSize, false ),
+ tileCenter = positionT.plus( sizeT.divide( 2 ) ),
+ tileDistance = viewportCenter.distanceTo( tileCenter );
+
+ if ( !overlap ) {
+ sizeC = sizeC.plus( new $.Point( 1, 1 ) );
+ }
+
+ tile.position = positionC;
+ tile.size = sizeC;
+ tile.distance = tileDistance;
+ tile.visibility = levelVisibility;
+}
+
+
+function blendTile( drawer, tile, x, y, level, levelOpacity, currentTime ){
+ var blendTimeMillis = 1000 * drawer.blendTime,
+ deltaTime,
+ opacity;
+
+ if ( !tile.blendStart ) {
+ tile.blendStart = currentTime;
+ }
+
+ deltaTime = currentTime - tile.blendStart;
+ opacity = blendTimeMillis ? Math.min( 1, deltaTime / ( blendTimeMillis ) ) : 1;
+
+ if ( drawer.alwaysBlend ) {
+ opacity *= levelOpacity;
+ }
+
+ tile.opacity = opacity;
+
+ drawer.lastDrawn.push( tile );
+
+ if ( opacity == 1 ) {
+ setCoverage( drawer.coverage, level, x, y, true );
+ } else if ( deltaTime < blendTimeMillis ) {
+ return true;
+ }
+
+ return false;
+}
+
+
+function clearTiles( drawer ) {
+ drawer.tilesMatrix = {};
+ drawer.tilesLoaded = [];
+}
+
+/**
+ * @private
+ * @inner
+ * Returns true if the given tile provides coverage to lower-level tiles of
+ * lower resolution representing the same content. If neither x nor y is
+ * given, returns true if the entire visible level provides coverage.
+ *
+ * Note that out-of-bounds tiles provide coverage in this sense, since
+ * there's no content that they would need to cover. Tiles at non-existent
+ * levels that are within the image bounds, however, do not.
+ */
+function providesCoverage( coverage, level, x, y ) {
+ var rows,
+ cols,
+ i, j;
+
+ if ( !coverage[ level ] ) {
+ return false;
+ }
+
+ if ( x === undefined || y === undefined ) {
+ rows = coverage[ level ];
+ for ( i in rows ) {
+ if ( rows.hasOwnProperty( i ) ) {
+ cols = rows[ i ];
+ for ( j in cols ) {
+ if ( cols.hasOwnProperty( j ) && !cols[ j ] ) {
+ return false;
+ }
+ }
+ }
+ }
+
+ return true;
+ }
+
+ return (
+ coverage[ level ][ x] === undefined ||
+ coverage[ level ][ x ][ y ] === undefined ||
+ coverage[ level ][ x ][ y ] === true
+ );
+}
+
+/**
+ * @private
+ * @inner
+ * Returns true if the given tile is completely covered by higher-level
+ * tiles of higher resolution representing the same content. If neither x
+ * nor y is given, returns true if the entire visible level is covered.
+ */
+function isCovered( coverage, level, x, y ) {
+ if ( x === undefined || y === undefined ) {
+ return providesCoverage( coverage, level + 1 );
+ } else {
+ return (
+ providesCoverage( coverage, level + 1, 2 * x, 2 * y ) &&
+ providesCoverage( coverage, level + 1, 2 * x, 2 * y + 1 ) &&
+ providesCoverage( coverage, level + 1, 2 * x + 1, 2 * y ) &&
+ providesCoverage( coverage, level + 1, 2 * x + 1, 2 * y + 1 )
+ );
+ }
+}
+
+/**
+ * @private
+ * @inner
+ * Sets whether the given tile provides coverage or not.
+ */
+function setCoverage( coverage, level, x, y, covers ) {
+ if ( !coverage[ level ] ) {
+ $.console.warn(
+ "Setting coverage for a tile before its level's coverage has been reset: %s",
+ level
+ );
+ return;
+ }
+
+ if ( !coverage[ level ][ x ] ) {
+ coverage[ level ][ x ] = {};
+ }
+
+ coverage[ level ][ x ][ y ] = covers;
+}
+
+/**
+ * @private
+ * @inner
+ * Resets coverage information for the given level. This should be called
+ * after every draw routine. Note that at the beginning of the next draw
+ * routine, coverage for every visible tile should be explicitly set.
+ */
+function resetCoverage( coverage, level ) {
+ coverage[ level ] = {};
+}
+
+/**
+ * @private
+ * @inner
+ * Determines whether the 'last best' tile for the area is better than the
+ * tile in question.
+ */
+function compareTiles( previousBest, tile ) {
+ if ( !previousBest ) {
+ return tile;
+ }
+
+ if ( tile.visibility > previousBest.visibility ) {
+ return tile;
+ } else if ( tile.visibility == previousBest.visibility ) {
+ if ( tile.distance < previousBest.distance ) {
+ return tile;
+ }
+ }
+
+ return previousBest;
+}
+
+function drawTiles( drawer, lastDrawn ){
+ var i,
+ tile,
+ tileKey,
+ viewer,
+ viewport,
+ position,
+ tileSource,
+ collectionTileSource;
+
+ // We need a callback to give image manipulation a chance to happen
+ var drawingHandler = function(args) {
+ if (drawer.viewer) {
+ /**
+ * This event is fired just before the tile is drawn giving the application a chance to alter the image.
+ *
+ * NOTE: This event is only fired when the drawer is using a