openseadragon/src/drawer.js

788 lines
23 KiB
JavaScript

(function( $ ){
// the max number of images we should keep in memory
var QUOTA = 100,
// the most shrunk a tile should be
MIN_PIXEL_RATIO = 0.5,
//TODO: make TIMEOUT configurable
TIMEOUT = 5000,
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 )
),
USE_CANVAS = $.isFunction( document.createElement( "canvas" ).getContext ) &&
SUBPIXEL_RENDERING;
$.Drawer = function(source, viewport, elmt) {
this.container = $.getElement( elmt );
this.canvas = $.makeNeutralElement( USE_CANVAS ? "canvas" : "div" );
this.context = USE_CANVAS ? this.canvas.getContext( "2d" ) : null;
this.viewport = viewport;
this.source = source;
this.config = this.viewport.config;
this.downloading = 0;
this.imageLoaderLimit = this.config.imageLoaderLimit;
//this.profiler = new $.Profiler();
this.minLevel = source.minLevel;
this.maxLevel = source.maxLevel;
this.tileSize = source.tileSize;
this.tileOverlap = source.tileOverlap;
this.normHeight = source.dimensions.y / source.dimensions.x;
// 1d dictionary [level] --> Point
this.cacheNumTiles = {};
// 1d dictionary [level] --> Point
this.cachePixelRatios = {};
// 3d dictionary [level][x][y] --> Tile
this.tilesMatrix = {};
// unordered list of Tiles with loaded images
this.tilesLoaded = [];
// 3d dictionary [level][x][y] --> Boolean
this.coverage = {};
// unordered list of Overlays added
this.overlays = [];
// unordered list of Tiles drawn last frame
this.lastDrawn = [];
this.lastResetTime = 0;
this.midUpdate = false;
this.updateAgain = true;
this.elmt = this.container;
this.canvas.style.width = "100%";
this.canvas.style.height = "100%";
this.canvas.style.position = "absolute";
// explicit left-align
this.container.style.textAlign = "left";
this.container.appendChild(this.canvas);
};
$.Drawer.prototype = {
getPixelRatio: function( level ) {
if ( !this.cachePixelRatios[ level ] ) {
this.cachePixelRatios[ level ] = this.source.getPixelRatio( level );
}
return this.cachePixelRatios[ level ];
},
_getTile: function( level, x, y, time, numTilesX, numTilesY ) {
var xMod,
yMod,
bounds,
exists,
url,
tile;
if ( !this.tilesMatrix[ level ] ) {
this.tilesMatrix[ level ] = {};
}
if ( !this.tilesMatrix[ level ][ x ] ) {
this.tilesMatrix[ level ][ x ] = {};
}
if ( !this.tilesMatrix[ level ][ x ][ y ] ) {
xMod = ( numTilesX + ( x % numTilesX ) ) % numTilesX;
yMod = ( numTilesY + ( y % numTilesY ) ) % numTilesY;
bounds = this.source.getTileBounds( level, xMod, yMod );
exists = this.source.tileExists( level, xMod, yMod );
url = this.source.getTileUrl( level, xMod, yMod );
bounds.x += 1.0 * ( x - xMod ) / numTilesX;
bounds.y += this.normHeight * ( y - yMod ) / numTilesY;
this.tilesMatrix[ level ][ x ][ y ] = new $.Tile(
level,
x,
y,
bounds,
exists,
url
);
}
tile = this.tilesMatrix[ level ][ x ][ y ];
tile.lastTouchTime = time;
return tile;
},
_loadTile: function( tile, time ) {
tile.loading = this.loadImage(
tile.url,
$.createCallback(
null,
$.delegate( this, this._onTileLoad ),
tile,
time
)
);
},
_onTileLoad: function( tile, time, image ) {
var insertionIndex,
cutoff,
worstTile,
worstTime,
worstLevel,
worstTileIndex,
prevTile,
prevTime,
prevLevel,
i;
tile.loading = false;
if ( this.midUpdate ) {
$.Debug.warn( "Tile load callback in middle of drawing routine." );
return;
} else if ( !image ) {
$.Debug.log( "Tile %s failed to load: %s", tile, tile.url );
tile.exists = false;
return;
} else if ( time < this.lastResetTime ) {
$.Debug.log( "Ignoring tile %s loaded before reset: %s", tile, tile.url );
return;
}
tile.loaded = true;
tile.image = image;
insertionIndex = this.tilesLoaded.length;
if ( this.tilesLoaded.length >= QUOTA ) {
cutoff = Math.ceil( Math.log( this.tileSize ) / Math.log( 2 ) );
worstTile = null;
worstTileIndex = -1;
for ( i = this.tilesLoaded.length - 1; i >= 0; i-- ) {
prevTile = this.tilesLoaded[ i ];
if ( prevTile.level <= this.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;
}
}
this.tilesLoaded[ insertionIndex ] = tile;
this.updateAgain = true;
},
_clearTiles: function() {
this.tilesMatrix = {};
this.tilesLoaded = [];
},
/**
* 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.
*/
_providesCoverage: function( level, x, y ) {
var rows,
cols,
i, j;
if ( !this.coverage[ level ] ) {
return false;
}
if ( x === undefined || y === undefined ) {
rows = this.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 (
this.coverage[ level ][ x] === undefined ||
this.coverage[ level ][ x ][ y ] === undefined ||
this.coverage[ level ][ x ][ y ] === true
);
},
/**
* 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.
*/
_isCovered: function( level, x, y ) {
if ( x === undefined || y === undefined ) {
return this._providesCoverage( level + 1 );
} else {
return (
this._providesCoverage( level + 1, 2 * x, 2 * y ) &&
this._providesCoverage( level + 1, 2 * x, 2 * y + 1 ) &&
this._providesCoverage( level + 1, 2 * x + 1, 2 * y ) &&
this._providesCoverage( level + 1, 2 * x + 1, 2 * y + 1 )
);
}
},
/**
* Sets whether the given tile provides coverage or not.
*/
_setCoverage: function( level, x, y, covers ) {
if ( !this.coverage[ level ] ) {
$.Debug.warn(
"Setting coverage for a tile before its level's coverage has been reset: %s",
level
);
return;
}
if ( !this.coverage[ level ][ x ] ) {
this.coverage[ level ][ x ] = {};
}
this.coverage[ level ][ x ][ y ] = covers;
},
/**
* 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.
*/
_resetCoverage: function( level ) {
this.coverage[ level ] = {};
},
_compareTiles: function( prevBest, tile ) {
if ( !prevBest ) {
return tile;
}
if ( tile.visibility > prevBest.visibility ) {
return tile;
} else if ( tile.visibility == prevBest.visibility ) {
if ( tile.distance < prevBest.distance ) {
return tile;
}
}
return prevBest;
},
_getOverlayIndex: function( elmt ) {
var i;
for ( i = this.overlays.length - 1; i >= 0; i-- ) {
if ( this.overlays[ i ].elmt == elmt ) {
return i;
}
}
return -1;
},
_updateActual: function() {
this.updateAgain = false;
var tile,
level,
viewportSize = this.viewport.getContainerSize(),
viewportWidth = viewportSize.x,
viewportHeight = viewportSize.y,
viewportBounds = this.viewport.getBounds( true ),
viewportTL = viewportBounds.getTopLeft(),
viewportBR = viewportBounds.getBottomRight(),
haveDrawn = false,
best = null,
currentTime = new Date().getTime(),
zeroRatioC = this.viewport.deltaPixelsFromPoints(
this.source.getPixelRatio( 0 ),
true
).x,
lowestLevel = Math.max(
this.minLevel,
Math.floor(
Math.log( this.config.minZoomImageRatio ) /
Math.log( 2 )
)
),
highestLevel = Math.min(
this.maxLevel,
Math.floor(
Math.log( zeroRatioC / MIN_PIXEL_RATIO ) /
Math.log( 2 )
)
);
//TODO
while ( this.lastDrawn.length > 0 ) {
tile = this.lastDrawn.pop();
tile.beingDrawn = false;
}
//TODO
this.canvas.innerHTML = "";
if ( USE_CANVAS ) {
this.canvas.width = viewportWidth;
this.canvas.height = viewportHeight;
this.context.clearRect( 0, 0, viewportWidth, viewportHeight );
}
//TODO
if ( !this.config.wrapHorizontal &&
( viewportBR.x < 0 || viewportTL.x > 1 ) ) {
return;
} else if
( !this.config.wrapVertical &&
( viewportBR.y < 0 || viewportTL.y > this.normHeight ) ) {
return;
}
//TODO
if ( !this.config.wrapHorizontal ) {
viewportTL.x = Math.max( viewportTL.x, 0 );
viewportBR.x = Math.min( viewportBR.x, 1 );
}
if ( !this.config.wrapVertical ) {
viewportTL.y = Math.max( viewportTL.y, 0 );
viewportBR.y = Math.min( viewportBR.y, this.normHeight );
}
//TODO
lowestLevel = Math.min( lowestLevel, highestLevel );
//TODO
for ( level = highestLevel; level >= lowestLevel; level-- ) {
//TODO
best = this._drawLevel(
level,
lowestLevel,
viewportTL,
viewportBR,
currentTime,
best
);
//TODO
if ( this._providesCoverage( level ) ) {
break;
}
}
//TODO
this._drawTiles();
this._drawOverlays();
//TODO
if ( best ) {
this._loadTile( best, currentTime );
// because we haven't finished drawing, so
this.updateAgain = true;
}
},
_drawLevel: function( level, lowestLevel, viewportTL, viewportBR, currentTime, best ){
var x, y,
levelOpacity,
levelVisibility,
drawTile,
tile,
tileTL,
tileBR,
numTiles,
numTilesX,
numTilesY,
renderPixelRatioC,
renderPixelRatioT,
levelOpacity,
levelVisibility,
haveDrawn = false,
drawLevel = false,
viewportCenter = this.viewport.pixelFromPoint( this.viewport.getCenter() ),
zeroRatioT = this.viewport.deltaPixelsFromPoints(
this.source.getPixelRatio( 0 ),
false
).x,
optimalRatio = this.config.immediateRender ?
1 :
zeroRatioT;
//Avoid calculations for draw if we have already drawn this
renderPixelRatioC = this.viewport.deltaPixelsFromPoints(
this.source.getPixelRatio( level ),
true
).x;
if ( ( !haveDrawn && renderPixelRatioC >= MIN_PIXEL_RATIO ) ||
( level == lowestLevel ) ) {
drawLevel = true;
haveDrawn = true;
} else if ( !haveDrawn ) {
return best;
}
//OK, a new drawing so do your calculations
tileTL = this.source.getTileAtPoint( level, viewportTL );
tileBR = this.source.getTileAtPoint( level, viewportBR );
numTiles = numberOfTiles( this, level );
numTilesX = numTiles.x;
numTilesY = numTiles.y;
renderPixelRatioT = this.viewport.deltaPixelsFromPoints(
this.source.getPixelRatio( level ),
false
).x;
levelOpacity = Math.min( 1, ( renderPixelRatioC - 0.5 ) / 0.5 );
levelVisibility = optimalRatio / Math.abs(
optimalRatio - renderPixelRatioT
);
this._resetCoverage( level );
if ( !this.config.wrapHorizontal ) {
tileBR.x = Math.min( tileBR.x, numTilesX - 1 );
}
if ( !this.config.wrapVertical ) {
tileBR.y = Math.min( tileBR.y, numTilesY - 1 );
}
for ( x = tileTL.x; x <= tileBR.x; x++ ) {
for ( y = tileTL.y; y <= tileBR.y; y++ ) {
tile = this._getTile(
level,
x, y,
currentTime,
numTilesX,
numTilesY
);
this._setCoverage( level, x, y, false );
if ( !tile.exists ) {
continue;
}
drawTile = drawLevel;
if ( haveDrawn && !drawTile ) {
if ( this._isCovered( level, x, y ) ) {
this._setCoverage( level, x, y, true );
} else {
drawTile = true;
}
}
if ( !drawTile ) {
continue;
}
this._positionTile(
tile,
viewportCenter,
levelVisibility
);
if ( tile.loaded ) {
updateAgain = this._blendTile(
tile,
x, y,
level,
levelOpacity,
currentTime
);
} else if ( tile.Loading ) {
//TODO: .Loading is never defined... did they mean .loading?
// but they didnt do anything so what is this block if
// if it does nothing.
} else {
best = this._compareTiles( best, tile );
}
}
}
return best;
},
_positionTile: function( tile, viewportCenter, levelVisibility ){
var boundsTL = tile.bounds.getTopLeft(),
boundsSize = tile.bounds.getSize(),
positionC = this.viewport.pixelFromPoint( boundsTL, true ),
sizeC = this.viewport.deltaPixelsFromPoints( boundsSize, true ),
positionT = this.viewport.pixelFromPoint( boundsTL, false ),
sizeT = this.viewport.deltaPixelsFromPoints( boundsSize, false ),
tileCenter = positionT.plus( sizeT.divide( 2 ) ),
tileDistance = viewportCenter.distanceTo( tileCenter );
if ( !this.tileOverlap ) {
sizeC = sizeC.plus( new $.Point( 1, 1 ) );
}
tile.position = positionC;
tile.size = sizeC;
tile.distance = tileDistance;
tile.visibility = levelVisibility;
},
_blendTile: function( tile, x, y, level, levelOpacity, currentTime ){
var blendTimeMillis = 1000 * this.config.blendTime,
deltaTime,
opacity;
if ( !tile.blendStart ) {
tile.blendStart = currentTime;
}
deltaTime = currentTime - tile.blendStart;
opacity = Math.min( 1, deltaTime / blendTimeMillis );
if ( this.config.alwaysBlend ) {
opacity *= levelOpacity;
}
tile.opacity = opacity;
this.lastDrawn.push( tile );
if ( opacity == 1 ) {
this._setCoverage( level, x, y, true );
} else if ( deltaTime < blendTimeMillis ) {
return true;
}
return false;
},
_drawTiles: function(){
var i,
tile;
for ( i = this.lastDrawn.length - 1; i >= 0; i-- ) {
tile = this.lastDrawn[ i ];
if ( USE_CANVAS ) {
tile.drawCanvas( this.context );
} else {
tile.drawHTML( this.canvas );
}
tile.beingDrawn = true;
}
},
_drawOverlays: function(){
var i,
length = this.overlays.length;
for ( i = 0; i < length; i++ ) {
this._drawOverlay( this.overlays[ i ] );
}
},
_drawOverlay: function( overlay ){
var bounds = overlay.bounds;
overlay.position = this.viewport.pixelFromPoint(
bounds.getTopLeft(),
true
);
overlay.size = this.viewport.deltaPixelsFromPoints(
bounds.getSize(),
true
);
overlay.drawHTML( this.container );
},
addOverlay: function( element, location, placement ) {
element = $.getElement( element );
if ( this._getOverlayIndex( element ) >= 0 ) {
// they're trying to add a duplicate overlay
return;
}
this.overlays.push( new $.Overlay( element, location, placement ) );
this.updateAgain = true;
},
updateOverlay: function( element, location, placement ) {
var i;
element = $.getElement( element );
i = this._getOverlayIndex( element );
if ( i >= 0 ) {
this.overlays[ i ].update( location, placement );
this.updateAgain = true;
}
},
removeOverlay: function( element ) {
var i;
element = $.getElement( element );
i = this._getOverlayIndex( element );
if ( i >= 0 ) {
this.overlays[ i ].destroy();
this.overlays.splice( i, 1 );
this.updateAgain = true;
}
},
clearOverlays: function() {
while ( this.overlays.length > 0 ) {
this.overlays.pop().destroy();
this.updateAgain = true;
}
},
needsUpdate: function() {
return this.updateAgain;
},
numTilesLoaded: function() {
return this.tilesLoaded.length;
},
reset: function() {
this._clearTiles();
this.lastResetTime = new Date().getTime();
this.updateAgain = true;
},
update: function() {
//this.profiler.beginUpdate();
this.midUpdate = true;
this._updateActual();
this.midUpdate = false;
//this.profiler.endUpdate();
},
loadImage: function(src, callback) {
var _this = this,
loading = false,
image,
jobid,
complete;
if ( !this.imageLoaderLimit || this.downloading < this.imageLoaderLimit ) {
this.downloading++;
image = new Image();
complete = function( imagesrc ){
_this.downloading--;
if (typeof ( callback ) == "function") {
try {
callback( image );
} catch ( e ) {
$.Debug.error(
"%s while executing %s callback: %s",
e.name,
src,
e.message,
e
);
}
}
};
image.onload = function(){
finishLoadingImage( image, complete, true );
};
image.onabort = image.onerror = function(){
finishLoadingImage( image, complete, false );
};
jobid = window.setTimeout( function(){
finishLoadingImage( image, complete, false, jobid );
}, TIMEOUT );
loading = true;
image.src = src;
}
return loading;
}
};
function finishLoadingImage( image, callback, successful, jobid ){
image.onload = null;
image.onabort = null;
image.onerror = null;
if ( jobid ) {
window.clearTimeout( jobid );
}
window.setTimeout( function() {
callback( image.src, successful ? image : null);
}, 1 );
};
function numberOfTiles( drawer, level ){
if ( !drawer.cacheNumTiles[ level ] ) {
drawer.cacheNumTiles[ level ] = drawer.source.getNumTiles( level );
}
return drawer.cacheNumTiles[ level ];
};
}( OpenSeadragon ));