openseadragon/src/tile.js

721 lines
26 KiB
JavaScript

/*
* OpenSeadragon - Tile
*
* Copyright (C) 2009 CodePlex Foundation
* Copyright (C) 2010-2024 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::addCache(...) 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::getTilePostData) 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;
/**
* Where this tile fits, in normalized coordinates, after positioning
* @member {OpenSeadragon.Rect} positionedBounds
* @memberof OpenSeadragon.Tile#
*/
this.positionedBounds = new OpenSeadragon.Rect(bounds.x, bounds.y, bounds.width, bounds.height);
/**
* 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::getTilePostData) 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);
}
this._cKey = cacheKey || "";
this._ocKey = 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;
/**
* Owner of this tile. Do not change this property manually.
* @member {OpenSeadragon.TiledImage}
* @memberof OpenSeadragon.Tile#
*/
this.tiledImage = null;
/**
* Array of cached tile data associated with the tile.
* @member {Object} _caches
* @private
*/
this._caches = {};
/**
* @private
*/
this._cacheSize = 0;
};
/** @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;
},
/**
* The unique main cache key for this tile. Created automatically
* from the given tiledImage.source.getTileHashKey(...) implementation.
* @member {String} cacheKey
* @memberof OpenSeadragon.Tile#
*/
get cacheKey() {
return this._cKey;
},
set cacheKey(value) {
if (value === this.cacheKey) {
return;
}
const cache = this.getCache(value);
if (!cache) {
// It's better to first set cache, then change the key to existing one. Warn if otherwise.
$.console.warn("[Tile.cacheKey] should not be set manually. Use addCache() with setAsMain=true.");
}
this._updateMainCacheKey(value);
},
/**
* 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#
*/
set originalCacheKey(value) {
throw "Original Cache Key cannot be managed manually!";
},
get originalCacheKey() {
return this._ocKey;
},
/**
* 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.getData] 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.getUrl] instead.");
return this.getUrl();
},
/**
* Get the Image object for this tile.
* @returns {?Image}
*/
getImage: function() {
$.console.error("[Tile.getImage] property has been deprecated. Use [Tile.getData] instead.");
//this method used to ensure the underlying data model conformed to given type - convert instead of getData()
const cache = this.getCache(this.cacheKey);
if (!cache) {
return undefined;
}
cache.transformTo("image");
return cache.data;
},
/**
* 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() {
$.console.error("[Tile.getCanvasContext] property has been deprecated. Use [Tile.getData] instead.");
//this method used to ensure the underlying data model conformed to given type - convert instead of getData()
const cache = this.getCache(this.cacheKey);
if (!cache) {
return undefined;
}
cache.transformTo("context2d");
return cache.data;
},
/**
* 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.getData] instead.");
return this.getCanvasContext();
},
/**
* 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.setData] instead.");
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::addCache.");
const cache = this._caches[this.cacheKey];
if (cache) {
this.removeCache(this.cacheKey);
}
if (value) {
// Note: the value's data is probably not preserved - if a cacheKey cache exists, it will ignore
// data - it would have to call setData(...)
// TODO: call setData() ?
if (value.loaded) {
this.addCache(this.cacheKey, value.data, value.type, true, false);
} else {
value.await().then(x => this.addCache(this.cacheKey, x, value.type, true, false));
}
}
},
/**
* Get the data to render for this tile
* @param {string} type data type to require
* @param {boolean} [copy=true] whether to force copy retrieval
* @return {*|undefined} data in the desired type, or undefined if a conversion is ongoing
*/
getData: function(type, copy = true) {
if (!this.tiledImage) {
return null; //async can access outside its lifetime
}
//we return the data synchronously immediatelly (undefined if conversion happens)
const cache = this.getCache(this.cacheKey);
if (!cache) {
$.console.error("[Tile::getData] There is no cache available for tile with key " + this.cacheKey);
return undefined;
}
return cache.getDataAs(type, copy);
},
/**
* Get the original data data for this tile
* @param {string} type data type to require
* @param {boolean} [copy=this.loaded] whether to force copy retrieval
* note that if you do not copy the data and save the data to a different cache,
* its destruction will also delete this original data which will likely cause issues
* @return {*|undefined} data in the desired type, or undefined if a conversion is ongoing
*/
getOriginalData: function(type, copy = true) {
if (!this.tiledImage) {
return null; //async can access outside its lifetime
}
//we return the data synchronously immediatelly (undefined if conversion happens)
const cache = this.getCache(this.originalCacheKey);
if (!cache) {
$.console.error("[Tile::getData] There is no cache available for tile with key " + this.originalCacheKey);
return undefined;
}
return cache.getDataAs(type, copy);
},
/**
* Set main cache data
* @param {*} value
* @param {?string} type data type to require
* @param {boolean} [preserveOriginalData=true] if true and cacheKey === originalCacheKey,
*/
setData: function(value, type, preserveOriginalData = true) {
if (!this.tiledImage) {
return null; //async can access outside its lifetime
}
if (preserveOriginalData && this.cacheKey === this.originalCacheKey) {
//caches equality means we have only one cache:
// create new cache record with main cache key changed to 'mod'
return this.addCache("mod://" + this.originalCacheKey, value, type, true)._promise;
}
//else overwrite cache
const cache = this.getCache(this.cacheKey);
if (!cache) {
$.console.error("[Tile::setData] There is no cache available for tile with key " + this.cacheKey);
return $.Promise.resolve();
}
return cache.setDataAs(value, type);
},
/**
* Read tile cache data object (CacheRecord)
* @param {string} [key=this.cacheKey] cache key to read that belongs to this tile
* @return {OpenSeadragon.CacheRecord}
*/
getCache: function(key = this._cKey) {
const cache = this._caches[key];
if (cache) {
cache.withTileReference(this);
}
return cache;
},
/**
* 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 IGNORED if cache already exists!
* @param {string} [type=undefined] data type, will be guessed if not provided
* @param {boolean} [setAsMain=false] if true, the key will be set as the tile.cacheKey
* @param [_safely=true] private
* @returns {OpenSeadragon.CacheRecord|null} - The cache record the tile was attached to.
*/
addCache: function(key, data, type = undefined, setAsMain = false, _safely = true) {
if (!this.tiledImage) {
return null; //async can access outside its lifetime
}
if (!type) {
if (!this.__typeWarningReported) {
$.console.warn(this, "[Tile.addCache] called without type specification. " +
"Automated deduction is potentially unsafe: prefer specification of data type explicitly.");
this.__typeWarningReported = true;
}
type = $.convertor.guessType(data);
}
const writesToRenderingCache = key === this.cacheKey;
if (writesToRenderingCache && _safely) {
// Need to get the supported type for rendering out of the active drawer.
const supportedTypes = this.tiledImage.viewer.drawer.getSupportedDataFormats();
const conversion = $.convertor.getConversionPath(type, supportedTypes);
$.console.assert(conversion, "[Tile.addCache] data was set for the default tile cache we are unable" +
"to render. Make sure OpenSeadragon.convertor was taught to convert to (one of): " + type);
}
const cachedItem = this.tiledImage._tileCache.cacheTile({
data: data,
dataType: type,
tile: this,
cacheKey: key,
//todo consider caching this on a tiled image level
cutoff: this.__cutoff || this.tiledImage.source.getClosestLevel(),
});
const havingRecord = this._caches[key];
if (havingRecord !== cachedItem) {
if (!havingRecord) {
this._cacheSize++;
}
this._caches[key] = cachedItem;
}
// Update cache key if differs and main requested
if (!writesToRenderingCache && setAsMain) {
this._updateMainCacheKey(key);
}
return cachedItem;
},
/**
* Sets the main cache key for this tile and
* performs necessary updates
* @param value
* @private
*/
_updateMainCacheKey: function(value) {
let ref = this._caches[this._cKey];
if (ref) {
// make sure we free drawer internal cache
ref.destroyInternalCache();
}
this._cKey = value;
// when key changes the image probably needs re-render
this.tiledImage.redraw();
},
/**
* Get the number of caches available to this tile
* @returns {number} number of caches
*/
getCacheSize: function() {
return this._cacheSize;
},
/**
* Free tile cache. Removes by default the cache record if no other tile uses it.
* @param {string} key cache key, required
* @param {boolean} [freeIfUnused=true] set to false if zombie should be created
*/
removeCache: function(key, freeIfUnused = true) {
if (!this._caches[key]) {
// try to erase anyway in case the cache got stuck in memory
this.tiledImage._tileCache.unloadCacheForTile(this, key, freeIfUnused);
return;
}
const currentMainKey = this.cacheKey,
originalDataKey = this.originalCacheKey,
sameBuiltinKeys = currentMainKey === originalDataKey;
if (!sameBuiltinKeys && originalDataKey === key) {
$.console.warn("[Tile.removeCache] original data must not be manually deleted: other parts of the code might rely on it!",
"If you want the tile not to preserve the original data, toggle of data perseverance in tile.setData().");
return;
}
if (currentMainKey === key) {
if (!sameBuiltinKeys && this._caches[originalDataKey]) {
// if we have original data let's revert back
// TODO consider calling drawer.getDataToDraw(...)
// or even better, first ensure the data is compatible and then update...?
this._updateMainCacheKey(originalDataKey);
} else {
$.console.warn("[Tile.removeCache] trying to remove the only cache that can be used to draw the tile!",
"If you want to remove the main cache, first set different cache as main with tile.addCache()");
return;
}
}
if (this.tiledImage._tileCache.unloadCacheForTile(this, key, freeIfUnused)) {
//if we managed to free tile from record, we are sure we decreased cache count
this._cacheSize--;
delete this._caches[key];
}
},
/**
* Get the ratio between current and original size.
* @function
* @deprecated
* @returns {number}
*/
getScaleForEdgeSmoothing: function() {
// getCanvasContext is deprecated and so should be this method.
$.console.warn("[Tile.getScaleForEdgeSmoothing] is deprecated, the following error is the consequence:");
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() {
//TODO AIOSA remove this.element and move it to a data constructor
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._cacheSize = 0;
this.element = null;
this.imgElement = null;
this.loaded = false;
this.loading = false;
this._cKey = this._ocKey;
}
};
}( OpenSeadragon ));