openseadragon/src/tile.js
Aiosa 219049976c Add tests for zombie and data type conversion, ensure destructors are called.
Fix bugs (zombie was disabled on item replace, fix zombie cache system by separating to its own cache array). Fix CacheRecord destructor & dijkstra. Deduce cache only from originalCacheKey. Force explicit type declaration with types on users.
2023-11-18 20:16:35 +01:00

691 lines
24 KiB
JavaScript

/*
* OpenSeadragon - Tile
*
* Copyright (C) 2009 CodePlex Foundation
* Copyright (C) 2010-2023 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( $ ){
/**
* @class Tile
* @memberof OpenSeadragon
* @param {Number} level The zoom level this tile belongs to.
* @param {Number} x The vector component 'x'.
* @param {Number} y The vector component 'y'.
* @param {OpenSeadragon.Rect} bounds Where this tile fits, in normalized
* coordinates.
* @param {Boolean} exists Is this tile a part of a sparse image? ( Also has
* this tile failed to load? )
* @param {String|Function} url The URL of this tile's image or a function that returns a url.
* @param {CanvasRenderingContext2D} [context2D=undefined] The context2D of this tile if it
* * is provided directly by the tile source. Deprecated: use Tile::setCache(...) instead.
* @param {Boolean} loadWithAjax Whether this tile image should be loaded with an AJAX request .
* @param {Object} ajaxHeaders The headers to send with this tile's AJAX request (if applicable).
* @param {OpenSeadragon.Rect} sourceBounds The portion of the tile to use as the source of the
* drawing operation, in pixels. Note that this only works when drawing with canvas; when drawing
* with HTML the entire tile is always used.
* @param {String} postData HTTP POST data (usually but not necessarily in k=v&k2=v2... form,
* see TileSource::getPostData) or null
* @param {String} cacheKey key to act as a tile cache, must be unique for tiles with unique image data
*/
$.Tile = function(level, x, y, bounds, exists, url, context2D, loadWithAjax, ajaxHeaders, sourceBounds, postData, cacheKey) {
/**
* The zoom level this tile belongs to.
* @member {Number} level
* @memberof OpenSeadragon.Tile#
*/
this.level = level;
/**
* The vector component 'x'.
* @member {Number} x
* @memberof OpenSeadragon.Tile#
*/
this.x = x;
/**
* The vector component 'y'.
* @member {Number} y
* @memberof OpenSeadragon.Tile#
*/
this.y = y;
/**
* Where this tile fits, in normalized coordinates
* @member {OpenSeadragon.Rect} bounds
* @memberof OpenSeadragon.Tile#
*/
this.bounds = bounds;
/**
* The portion of the tile to use as the source of the drawing operation, in pixels. Note that
* this only works when drawing with canvas; when drawing with HTML the entire tile is always used.
* @member {OpenSeadragon.Rect} sourceBounds
* @memberof OpenSeadragon.Tile#
*/
this.sourceBounds = sourceBounds;
/**
* Is this tile a part of a sparse image? Also has this tile failed to load?
* @member {Boolean} exists
* @memberof OpenSeadragon.Tile#
*/
this.exists = exists;
/**
* Private property to hold string url or url retriever function.
* Consumers should access via Tile.getUrl()
* @private
* @member {String|Function} url
* @memberof OpenSeadragon.Tile#
*/
this._url = url;
/**
* Post parameters for this tile. For example, it can be an URL-encoded string
* in k1=v1&k2=v2... format, or a JSON, or a FormData instance... or null if no POST request used
* @member {String} postData HTTP POST data (usually but not necessarily in k=v&k2=v2... form,
* see TileSource::getPostData) or null
* @memberof OpenSeadragon.Tile#
*/
this.postData = postData;
/**
* The context2D of this tile if it is provided directly by the tile source.
* @member {CanvasRenderingContext2D} context2D
* @memberOf OpenSeadragon.Tile#
*/
if (context2D) {
this.context2D = context2D;
}
/**
* Whether to load this tile's image with an AJAX request.
* @member {Boolean} loadWithAjax
* @memberof OpenSeadragon.Tile#
*/
this.loadWithAjax = loadWithAjax;
/**
* The headers to be used in requesting this tile's image.
* Only used if loadWithAjax is set to true.
* @member {Object} ajaxHeaders
* @memberof OpenSeadragon.Tile#
*/
this.ajaxHeaders = ajaxHeaders;
if (cacheKey === undefined) {
$.console.warn("Tile constructor needs 'cacheKey' variable: creation tile cache" +
" in Tile class is deprecated. TileSource.prototype.getTileHashKey will be used.");
cacheKey = $.TileSource.prototype.getTileHashKey(level, x, y, url, ajaxHeaders, postData);
}
/**
* The unique main cache key for this tile. Created automatically
* from the given tiledImage.source.getTileHashKey(...) implementation.
* @member {String} cacheKey
* @memberof OpenSeadragon.Tile#
*/
this.cacheKey = cacheKey;
/**
* By default equal to tile.cacheKey, marks a cache associated with this tile
* that holds the cache original data (it was loaded with). In case you
* change the tile data, the tile original data should be left with the cache
* 'originalCacheKey' and the new, modified data should be stored in cache 'cacheKey'.
* This key is used in cache resolution: in case new tile data is requested, if
* this cache key exists in the cache it is loaded.
* @member {String} originalCacheKey
* @memberof OpenSeadragon.Tile#
*/
this.originalCacheKey = this.cacheKey;
/**
* Is this tile loaded?
* @member {Boolean} loaded
* @memberof OpenSeadragon.Tile#
*/
this.loaded = false;
/**
* Is this tile loading?
* @member {Boolean} loading
* @memberof OpenSeadragon.Tile#
*/
this.loading = false;
/**
* The HTML div element for this tile
* @member {Element} element
* @memberof OpenSeadragon.Tile#
*/
this.element = null;
/**
* The HTML img element for this tile.
* @member {Element} imgElement
* @memberof OpenSeadragon.Tile#
*/
this.imgElement = null;
/**
* The alias of this.element.style.
* @member {String} style
* @memberof OpenSeadragon.Tile#
*/
this.style = null;
/**
* This tile's position on screen, in pixels.
* @member {OpenSeadragon.Point} position
* @memberof OpenSeadragon.Tile#
*/
this.position = null;
/**
* This tile's size on screen, in pixels.
* @member {OpenSeadragon.Point} size
* @memberof OpenSeadragon.Tile#
*/
this.size = null;
/**
* Whether to flip the tile when rendering.
* @member {Boolean} flipped
* @memberof OpenSeadragon.Tile#
*/
this.flipped = false;
/**
* The start time of this tile's blending.
* @member {Number} blendStart
* @memberof OpenSeadragon.Tile#
*/
this.blendStart = null;
/**
* The current opacity this tile should be.
* @member {Number} opacity
* @memberof OpenSeadragon.Tile#
*/
this.opacity = null;
/**
* The squared distance of this tile to the viewport center.
* Use for comparing tiles.
* @private
* @member {Number} squaredDistance
* @memberof OpenSeadragon.Tile#
*/
this.squaredDistance = null;
/**
* The visibility score of this tile.
* @member {Number} visibility
* @memberof OpenSeadragon.Tile#
*/
this.visibility = null;
/**
* The transparency indicator of this tile.
* @member {Boolean} hasTransparency true if tile contains transparency for correct rendering
* @memberof OpenSeadragon.Tile#
*/
this.hasTransparency = false;
/**
* Whether this tile is currently being drawn.
* @member {Boolean} beingDrawn
* @memberof OpenSeadragon.Tile#
*/
this.beingDrawn = false;
/**
* Timestamp the tile was last touched.
* @member {Number} lastTouchTime
* @memberof OpenSeadragon.Tile#
*/
this.lastTouchTime = 0;
/**
* Whether this tile is in the right-most column for its level.
* @member {Boolean} isRightMost
* @memberof OpenSeadragon.Tile#
*/
this.isRightMost = false;
/**
* Whether this tile is in the bottom-most row for its level.
* @member {Boolean} isBottomMost
* @memberof OpenSeadragon.Tile#
*/
this.isBottomMost = false;
/**
* FIXME: I would like to remove this reference but there is no way
* to remove it since tile-unloaded event requires the tiledImage reference.
* And, unloadTilesFor(tiledImage) in cache uses it too. Storing the
* reference on a tile level rather than cache level is more efficient.
*
* Owner of this tile.
* @member {OpenSeadragon.TiledImage}
* @memberof OpenSeadragon.Tile#
*/
this.tiledImage = null;
/**
* Array of cached tile data associated with the tile.
* @member {Object} _caches
* @private
*/
this._caches = {};
};
/** @lends OpenSeadragon.Tile.prototype */
$.Tile.prototype = {
/**
* Provides a string representation of this tiles level and (x,y)
* components.
* @function
* @returns {String}
*/
toString: function() {
return this.level + "/" + this.x + "_" + this.y;
},
/**
* Renders the tile in an html container.
* @function
* @param {Element} container
*/
drawHTML: function( container ) {
if ( !this.loaded ) {
$.console.warn(
"Attempting to draw tile %s when it's not yet loaded.",
this.toString()
);
return;
}
//EXPERIMENTAL - trying to figure out how to scale the container
// content during animation of the container size.
if ( !this.element ) {
const image = this.getImage();
if (!image) {
$.console.warn(
'[Tile.drawHTML] attempting to draw tile %s when it\'s not cached',
this.toString());
return;
}
this.element = $.makeNeutralElement( "div" );
this.imgElement = image.cloneNode();
this.imgElement.style.msInterpolationMode = "nearest-neighbor";
this.imgElement.style.width = "100%";
this.imgElement.style.height = "100%";
this.style = this.element.style;
this.style.position = "absolute";
}
if ( this.element.parentNode !== container ) {
container.appendChild( this.element );
}
if ( this.imgElement.parentNode !== this.element ) {
this.element.appendChild( this.imgElement );
}
this.style.top = this.position.y + "px";
this.style.left = this.position.x + "px";
this.style.height = this.size.y + "px";
this.style.width = this.size.x + "px";
if (this.flipped) {
this.style.transform = "scaleX(-1)";
}
$.setElementOpacity( this.element, this.opacity );
},
/**
* The Image object for this tile.
* @member {Object} image
* @memberof OpenSeadragon.Tile#
* @deprecated
* @returns {Image}
*/
get image() {
$.console.error("[Tile.image] property has been deprecated. Use [Tile.prototype.getImage] instead.");
return this.getImage();
},
/**
* The URL of this tile's image.
* @member {String} url
* @memberof OpenSeadragon.Tile#
* @deprecated
* @returns {String}
*/
get url() {
$.console.error("[Tile.url] property has been deprecated. Use [Tile.prototype.getUrl] instead.");
return this.getUrl();
},
/**
* Get the Image object for this tile.
* @returns {?Image}
*/
getImage: function() {
return this.getData("image");
},
/**
* Get the url string for this tile.
* @returns {String}
*/
getUrl: function() {
if (typeof this._url === 'function') {
return this._url();
}
return this._url;
},
/**
* Get the CanvasRenderingContext2D instance for tile image data drawn
* onto Canvas if enabled and available
* @returns {?CanvasRenderingContext2D}
*/
getCanvasContext: function() {
return this.getData("context2d");
},
/**
* The context2D of this tile if it is provided directly by the tile source.
* @deprecated
* @type {CanvasRenderingContext2D} context2D
*/
get context2D() {
$.console.error("[Tile.context2D] property has been deprecated. Use Tile::getCache().");
return this.getData("context2d");
},
/**
* The context2D of this tile if it is provided directly by the tile source.
* @deprecated
*/
set context2D(value) {
$.console.error("[Tile.context2D] property has been deprecated. Use Tile::setCache().");
this.setData(value, "context2d");
},
/**
* The default cache for this tile.
* @deprecated
* @type OpenSeadragon.CacheRecord
*/
get cacheImageRecord() {
$.console.error("[Tile.cacheImageRecord] property has been deprecated. Use Tile::getCache.");
return this.getCache(this.cacheKey);
},
/**
* The default cache for this tile.
* @deprecated
*/
set cacheImageRecord(value) {
$.console.error("[Tile.cacheImageRecord] property has been deprecated. Use Tile::setCache.");
this._caches[this.cacheKey] = value;
},
/**
* Get the default data for this tile
* @param {?string} [type=undefined] data type to require
* @return {*|undefined} data in the desired type, or undefined if a conversion is ongoing
*/
getData(type = undefined) {
const cache = this.getCache(this.cacheKey);
if (!cache) {
return undefined;
}
cache.getData(type); //returns a promise
//we return the data synchronously immediatelly (undefined if conversion happens)
return cache.data;
},
/**
* Invalidate the tile so that viewport gets updated.
*/
save() {
const parent = this.tiledImage;
if (parent) {
parent._needsDraw = true;
}
},
/**
* Set cache data
* @param {*} value
* @param {?string} [type=undefined] data type to require
*/
setData(value, type = undefined) {
this.setCache(this.cacheKey, value, type);
},
/**
* Read tile cache data object (CacheRecord)
* @param {string} key cache key to read that belongs to this tile
* @return {OpenSeadragon.CacheRecord}
*/
getCache: function(key) {
return this._caches[key];
},
/**
* Set tile cache, possibly multiple with custom key
* @param {string} key cache key, must be unique (we recommend re-using this.cacheTile
* value and extend it with some another unique content, by default overrides the existing
* main cache used for drawing, if not existing.
* @param {*} data data to cache - this data will be sent to the TileSource API for refinement.
* @param {?string} type data type, will be guessed if not provided
* @param [_safely=true] private
* @param [_cutoff=0] private
*/
setCache: function(key, data, type = undefined, _safely = true, _cutoff = 0) {
if (!type && this.tiledImage && !this.tiledImage.__typeWarningReported) {
$.console.warn(this, "[Tile.setCache] called without type specification. " +
"Automated deduction is potentially unsafe: prefer specification of data type explicitly.");
this.tiledImage.__typeWarningReported = true;
type = $.convertor.guessType(data);
}
if (_safely && key === this.cacheKey) {
//todo later, we could have drawers register their supported rendering type
// and OpenSeadragon would check compatibility automatically, now we render
// using two main types so we check their ability
const conversion = $.convertor.getConversionPath(type, "canvas", "image");
$.console.assert(conversion, "[Tile.setCache] data was set for the default tile cache we are unable" +
"to render. Make sure OpenSeadragon.convertor was taught to convert type: " + type);
}
this.tiledImage._tileCache.cacheTile({
data: data,
dataType: type,
tile: this,
cacheKey: key,
cutoff: _cutoff
});
},
/**
* Renders the tile in a canvas-based context.
* @function
* @param {CanvasRenderingContext2D} context
* @param {Function} drawingHandler - Method for firing the drawing event.
* drawingHandler({context, tile, rendered})
* where <code>rendered</code> is the context with the pre-drawn image.
* @param {Number} [scale=1] - Apply a scale to position and size
* @param {OpenSeadragon.Point} [translate] - A translation vector
* @param {Boolean} [shouldRoundPositionAndSize] - Tells whether to round
* position and size of tiles supporting alpha channel in non-transparency context.
* @param {OpenSeadragon.TileSource} source - The source specification of the tile.
*/
drawCanvas: function( context, drawingHandler, scale, translate, shouldRoundPositionAndSize, source) {
var position = this.position.times($.pixelDensityRatio),
size = this.size.times($.pixelDensityRatio),
rendered = this.getCanvasContext();
if (!rendered) {
$.console.warn(
'[Tile.drawCanvas] attempting to draw tile %s when it\'s not cached',
this.toString());
return;
}
if ( !this.loaded || !rendered ){
$.console.warn(
"Attempting to draw tile %s when it's not yet loaded.",
this.toString()
);
return;
}
context.save();
context.globalAlpha = this.opacity;
if (typeof scale === 'number' && scale !== 1) {
// draw tile at a different scale
position = position.times(scale);
size = size.times(scale);
}
if (translate instanceof $.Point) {
// shift tile position slightly
position = position.plus(translate);
}
//if we are supposed to be rendering fully opaque rectangle,
//ie its done fading or fading is turned off, and if we are drawing
//an image with an alpha channel, then the only way
//to avoid seeing the tile underneath is to clear the rectangle
if (context.globalAlpha === 1 && this.hasTransparency) {
if (shouldRoundPositionAndSize) {
// Round to the nearest whole pixel so we don't get seams from overlap.
position.x = Math.round(position.x);
position.y = Math.round(position.y);
size.x = Math.round(size.x);
size.y = Math.round(size.y);
}
//clearing only the inside of the rectangle occupied
//by the png prevents edge flikering
context.clearRect(
position.x,
position.y,
size.x,
size.y
);
}
// This gives the application a chance to make image manipulation
// changes as we are rendering the image
drawingHandler({context: context, tile: this, rendered: rendered});
var sourceWidth, sourceHeight;
if (this.sourceBounds) {
sourceWidth = Math.min(this.sourceBounds.width, rendered.canvas.width);
sourceHeight = Math.min(this.sourceBounds.height, rendered.canvas.height);
} else {
sourceWidth = rendered.canvas.width;
sourceHeight = rendered.canvas.height;
}
context.translate(position.x + size.x / 2, 0);
if (this.flipped) {
context.scale(-1, 1);
}
context.drawImage(
rendered.canvas,
0,
0,
sourceWidth,
sourceHeight,
-size.x / 2,
position.y,
size.x,
size.y
);
context.restore();
},
/**
* Get the ratio between current and original size.
* @function
* @returns {Number}
*/
getScaleForEdgeSmoothing: function() {
const context = this.getCanvasContext();
if (!context) {
$.console.warn(
'[Tile.drawCanvas] attempting to get tile scale %s when tile\'s not cached',
this.toString());
return 1;
}
return context.canvas.width / (this.size.x * $.pixelDensityRatio);
},
/**
* Get a translation vector that when applied to the tile position produces integer coordinates.
* Needed to avoid swimming and twitching.
* @function
* @param {Number} [scale=1] - Scale to be applied to position.
* @returns {OpenSeadragon.Point}
*/
getTranslationForEdgeSmoothing: function(scale, canvasSize, sketchCanvasSize) {
// The translation vector must have positive values, otherwise the image goes a bit off
// the sketch canvas to the top and left and we must use negative coordinates to repaint it
// to the main canvas. In that case, some browsers throw:
// INDEX_SIZE_ERR: DOM Exception 1: Index or size was negative, or greater than the allowed value.
var x = Math.max(1, Math.ceil((sketchCanvasSize.x - canvasSize.x) / 2));
var y = Math.max(1, Math.ceil((sketchCanvasSize.y - canvasSize.y) / 2));
return new $.Point(x, y).minus(
this.position
.times($.pixelDensityRatio)
.times(scale || 1)
.apply(function(x) {
return x % 1;
})
);
},
/**
* Removes tile from its container.
* @function
*/
unload: function() {
if ( this.imgElement && this.imgElement.parentNode ) {
this.imgElement.parentNode.removeChild( this.imgElement );
}
if ( this.element && this.element.parentNode ) {
this.element.parentNode.removeChild( this.element );
}
this.tiledImage = null;
this._caches = [];
this.element = null;
this.imgElement = null;
this.loaded = false;
this.loading = false;
}
};
}( OpenSeadragon ));