Compare commits

...

3 Commits

Author SHA1 Message Date
Ian Gilman
eb85b4498f
Merge pull request #2247 from JohnReagan/1970/support-tile-source-url-getter
Add support for dynamic urls from tile source. Fixes #1970
2022-12-02 14:16:40 -08:00
John Reagan
8ef5270d7f #1970: Update unit test to check for changed url behavior 2022-12-02 10:32:11 -05:00
John Reagan
601160e9fb Add support for dynamic urls from tile source. Fixes #1970 2022-11-30 15:38:57 -05:00
5 changed files with 241 additions and 14 deletions

View File

@ -44,7 +44,7 @@
* coordinates. * coordinates.
* @param {Boolean} exists Is this tile a part of a sparse image? ( Also has * @param {Boolean} exists Is this tile a part of a sparse image? ( Also has
* this tile failed to load? ) * this tile failed to load? )
* @param {String} url The URL of this tile's image. * @param {String|() => String} url The URL of this tile's image or a function that returns a url.
* @param {CanvasRenderingContext2D} context2D The context2D of this tile if it * @param {CanvasRenderingContext2D} context2D The context2D of this tile if it
* is provided directly by the tile source. * is provided directly by the tile source.
* @param {Boolean} loadWithAjax Whether this tile image should be loaded with an AJAX request . * @param {Boolean} loadWithAjax Whether this tile image should be loaded with an AJAX request .
@ -95,11 +95,13 @@ $.Tile = function(level, x, y, bounds, exists, url, context2D, loadWithAjax, aja
*/ */
this.exists = exists; this.exists = exists;
/** /**
* The URL of this tile's image. * Private property to hold string url or url retriever function.
* @member {String} url * Consumers should access via Tile.getUrl()
* @private
* @member {String|() => String} url
* @memberof OpenSeadragon.Tile# * @memberof OpenSeadragon.Tile#
*/ */
this.url = url; this._url = url;
/** /**
* Post parameters for this tile. For example, it can be an URL-encoded string * 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 * in k1=v1&k2=v2... format, or a JSON, or a FormData instance... or null if no POST request used
@ -269,7 +271,7 @@ $.Tile.prototype = {
_hasTransparencyChannel: function() { _hasTransparencyChannel: function() {
console.warn("Tile.prototype._hasTransparencyChannel() has been " + console.warn("Tile.prototype._hasTransparencyChannel() has been " +
"deprecated and will be removed in the future. Use TileSource.prototype.hasTransparency() instead."); "deprecated and will be removed in the future. Use TileSource.prototype.hasTransparency() instead.");
return !!this.context2D || this.url.match('.png'); return !!this.context2D || this.getUrl().match('.png');
}, },
/** /**
@ -342,6 +344,18 @@ $.Tile.prototype = {
return this.getImage(); 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. * Get the Image object for this tile.
* @returns {Image} * @returns {Image}
@ -350,6 +364,18 @@ $.Tile.prototype = {
return this.cacheImageRecord.getImage(); return this.cacheImageRecord.getImage();
}, },
/**
* 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 * Get the CanvasRenderingContext2D instance for tile image data drawn
* onto Canvas if enabled and available * onto Canvas if enabled and available

View File

@ -1480,7 +1480,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
bounds, bounds,
sourceBounds, sourceBounds,
exists, exists,
url, urlOrGetter,
post, post,
ajaxHeaders, ajaxHeaders,
context2D, context2D,
@ -1501,7 +1501,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
bounds = this.getTileBounds( level, x, y ); bounds = this.getTileBounds( level, x, y );
sourceBounds = tileSource.getTileBounds( level, xMod, yMod, true ); sourceBounds = tileSource.getTileBounds( level, xMod, yMod, true );
exists = tileSource.tileExists( level, xMod, yMod ); exists = tileSource.tileExists( level, xMod, yMod );
url = tileSource.getTileUrl( level, xMod, yMod ); urlOrGetter = tileSource.getTileUrl( level, xMod, yMod );
post = tileSource.getTilePostData( level, xMod, yMod ); post = tileSource.getTilePostData( level, xMod, yMod );
// Headers are only applicable if loadTilesWithAjax is set // Headers are only applicable if loadTilesWithAjax is set
@ -1524,13 +1524,13 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
y, y,
bounds, bounds,
exists, exists,
url, urlOrGetter,
context2D, context2D,
this.loadTilesWithAjax, this.loadTilesWithAjax,
ajaxHeaders, ajaxHeaders,
sourceBounds, sourceBounds,
post, post,
tileSource.getTileHashKey(level, xMod, yMod, url, ajaxHeaders, post) tileSource.getTileHashKey(level, xMod, yMod, urlOrGetter, ajaxHeaders, post)
); );
if (this.getFlip()) { if (this.getFlip()) {
@ -1569,7 +1569,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
var _this = this; var _this = this;
tile.loading = true; tile.loading = true;
this._imageLoader.addJob({ this._imageLoader.addJob({
src: tile.url, src: tile.getUrl(),
tile: tile, tile: tile,
source: this.source, source: this.source,
postData: tile.postData, postData: tile.postData,
@ -1598,7 +1598,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
*/ */
_onTileLoad: function( tile, time, data, errorMsg, tileRequest ) { _onTileLoad: function( tile, time, data, errorMsg, tileRequest ) {
if ( !data ) { if ( !data ) {
$.console.error( "Tile %s failed to load: %s - error: %s", tile, tile.url, errorMsg ); $.console.error( "Tile %s failed to load: %s - error: %s", tile, tile.getUrl(), errorMsg );
/** /**
* Triggered when a tile fails to load. * Triggered when a tile fails to load.
* *
@ -1624,7 +1624,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
} }
if ( time < this.lastResetTime ) { if ( time < this.lastResetTime ) {
$.console.warn( "Ignoring tile %s loaded before reset: %s", tile, tile.url ); $.console.warn( "Ignoring tile %s loaded before reset: %s", tile, tile.getUrl() );
tile.loading = false; tile.loading = false;
return; return;
} }
@ -1669,7 +1669,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
tile.loading = false; tile.loading = false;
tile.loaded = true; tile.loaded = true;
tile.hasTransparency = _this.source.hasTransparency( tile.hasTransparency = _this.source.hasTransparency(
tile.context2D, tile.url, tile.ajaxHeaders, tile.postData tile.context2D, tile.getUrl(), tile.ajaxHeaders, tile.postData
); );
if (!tile.context2D) { if (!tile.context2D) {
_this._tileCache.cacheTile({ _this._tileCache.cacheTile({
@ -1852,7 +1852,7 @@ $.extend($.TiledImage.prototype, $.EventSource.prototype, /** @lends OpenSeadrag
useSketch = this.opacity < 1 || useSketch = this.opacity < 1 ||
(this.compositeOperation && this.compositeOperation !== 'source-over') || (this.compositeOperation && this.compositeOperation !== 'source-over') ||
(!this._isBottomItem() && (!this._isBottomItem() &&
this.source.hasTransparency(tile.context2D, tile.url, tile.ajaxHeaders, tile.postData)); this.source.hasTransparency(tile.context2D, tile.getUrl(), tile.ajaxHeaders, tile.postData));
} }
var sketchScale; var sketchScale;

View File

@ -618,6 +618,7 @@ $.TileSource.prototype = {
* @param {Number} level * @param {Number} level
* @param {Number} x * @param {Number} x
* @param {Number} y * @param {Number} y
* @returns {String|() => string} url - A string for the url or a function that returns a url string.
* @throws {Error} * @throws {Error}
*/ */
getTileUrl: function( level, x, y ) { getTileUrl: function( level, x, y ) {

View File

@ -0,0 +1,199 @@
/* global QUnit, testLog, Util */
//Testing of TileSource with getTileUrl that returns a function
(function() {
var ASSERT = null;
var DYNAMIC_URL = "";
var viewer = null;
var OriginalAjax = OpenSeadragon.makeAjaxRequest;
var OriginalTile = OpenSeadragon.Tile;
// These variables allow tracking when the first request for data has finished
var firstUrlPromise = null;
var isFirstUrlPromiseResolved = false;
var firstUrlPromiseResolver = null;
/**
* Set up shared variables for test
*/
var configure = function(assert, url) {
ASSERT = assert;
DYNAMIC_URL = url;
firstUrlPromise = new Promise(resolve => {
firstUrlPromiseResolver = () => {
isFirstUrlPromiseResolved = true;
resolve();
};
});
};
QUnit.module('TileSourceDynamicUrl', {
beforeEach: function () {
testLog.reset();
$("#qunit-fixture").html("<div id='example'></div>");
// Add test tile source to OSD
OpenSeadragon.DynamicUrlTestTileSource = function(options) {
OpenSeadragon.TileSource.apply(this, [options]);
};
OpenSeadragon.extend( OpenSeadragon.DynamicUrlTestTileSource.prototype, OpenSeadragon.TileSource.prototype, {
supports: function(_data, url){
return url.indexOf('dynamic') !== -1;
},
configure: function(_data, url, postData){
//some default data to trigger painting
return {
postData: postData,
tilesUrl: url,
fileFormat: "jpg",
sizeData: {Width: 55, Height: 55},
width: 55,
height: 55,
tileSize: 55,
tileOverlap: 55,
minLevel: 1,
maxLevel: 1,
displayRects: []
};
},
// getTileUrl return a function that must be called by Tile.getUrl
getTileUrl: function(_level, _x, _y) {
// Assert that custom tile source is called correctly
ASSERT.ok(true, 'DynamicUrlTileSource.getTileUrl called');
return () => DYNAMIC_URL;
},
tileExists: function (_level, _x, _y) {
return true;
}
});
var hasCompletedImageInfoRequest = false;
OpenSeadragon.makeAjaxRequest = function(url, onSuccess, onError) {
// Note that our preferred API is that you pass in a single object; the named
// arguments are for legacy support.
if( $.isPlainObject(url)){
onSuccess = url.success;
onError = url.error;
withCredentials = url.withCredentials;
headers = url.headers;
responseType = url.responseType || null;
postData = url.postData || null;
options = url; //save original stuff
url = url.url;
}
//first AJAX firing is the image info getter, second is the first tile request: can exit
if (hasCompletedImageInfoRequest) {
// Assert dynamic url from tileSource is called
ASSERT.equal(url, DYNAMIC_URL, 'Called dynamic url correctly: ' + DYNAMIC_URL);
// If we've only queried for one url, resolve that promise to set up second query
// Otherwise close viewer
if (isFirstUrlPromiseResolved) {
viewer.close();
} else {
firstUrlPromiseResolver();
}
return null;
}
hasCompletedImageInfoRequest = true;
var request = Promise.resolve(url);
//some required properties to pass through processResponse(...)
request.responseText = "some text";
request.status = 200;
onSuccess(request);
return request;
};
// Override Tile to ensure getUrl is called successfully.
var Tile = function(...params) {
OriginalTile.apply(this, params);
};
OpenSeadragon.extend( Tile.prototype, OpenSeadragon.Tile.prototype, {
getUrl: function() {
ASSERT.ok(true, 'Tile.getUrl called');
return OriginalTile.prototype.getUrl.apply(this);
}
});
OpenSeadragon.Tile = Tile;
},
afterEach: function () {
ASSERT = null;
if (viewer && viewer.close) {
viewer.close();
}
viewer = null;
OpenSeadragon.makeAjaxRequest = OriginalAjax;
OpenSeadragon.Tile = OriginalTile;
}
});
/**
* Create viewer for test
*/
var testUrlCall = function(tileSourceUrl) {
var timeWatcher = Util.timeWatcher(ASSERT, 7000);
viewer = OpenSeadragon({
id: 'example',
prefixUrl: '/build/openseadragon/images/',
tileSources: tileSourceUrl,
loadTilesWithAjax: true,
});
var failHandler = function (event) {
testPostData(event.postData, "event: 'open-failed'");
viewer.removeHandler('open-failed', failHandler);
viewer.close();
};
viewer.addHandler('open-failed', failHandler);
var readyHandler = function (event) {
//relies on Tilesource contructor extending itself with options object
testPostData(event.postData, "event: 'ready'");
viewer.removeHandler('ready', readyHandler);
};
viewer.addHandler('ready', readyHandler);
var openHandler = function(_event) {
viewer.removeHandler('open', openHandler);
ASSERT.ok(true, 'Open event was sent');
viewer.addHandler('close', closeHandler);
viewer.world.draw();
};
var closeHandler = function(_event) {
viewer.removeHandler('close', closeHandler);
$('#example').empty();
ASSERT.ok(true, 'Close event was sent');
timeWatcher.done();
};
viewer.addHandler('open', openHandler);
return viewer;
};
// ----------
QUnit.test('TileSource.getTileUrl supports returning a function', function(assert) {
configure(assert, 'dynamicUrl');
const viewer = testUrlCall('dynamicUrl');
firstUrlPromise.then(() => {
// after querying with first dynamic url, update the url and trigger new request
DYNAMIC_URL = 'dyanmicUrl2';
delete viewer.world.getItemAt(0).tilesMatrix[1][0][0];
})
});
})();

View File

@ -47,6 +47,7 @@
<script src="/test/modules/ajax-tiles.js"></script> <script src="/test/modules/ajax-tiles.js"></script>
<script src="/test/modules/ajax-post-data.js"></script> <script src="/test/modules/ajax-post-data.js"></script>
<script src="/test/modules/imageloader.js"></script> <script src="/test/modules/imageloader.js"></script>
<script src="/test/modules/tilesource-dynamic-url.js"></script>
<!--The navigator tests are the slowest (for now; hopefully they can be sped up) <!--The navigator tests are the slowest (for now; hopefully they can be sped up)
so we put them last. --> so we put them last. -->
<script src="/test/modules/navigator.js"></script> <script src="/test/modules/navigator.js"></script>