2017-12-07 04:20:39 +03:00
|
|
|
/* global QUnit, testLog */
|
2014-11-20 22:51:24 +03:00
|
|
|
|
|
|
|
(function() {
|
2023-11-26 23:32:26 +03:00
|
|
|
const Convertor = OpenSeadragon.convertor,
|
|
|
|
T_A = "__TEST__typeA", T_B = "__TEST__typeB", T_C = "__TEST__typeC", T_D = "__TEST__typeD", T_E = "__TEST__typeE";
|
|
|
|
|
2023-11-18 22:16:35 +03:00
|
|
|
let viewer;
|
|
|
|
|
|
|
|
//we override jobs: remember original function
|
|
|
|
const originalJob = OpenSeadragon.ImageLoader.prototype.addJob;
|
|
|
|
|
|
|
|
//event awaiting
|
|
|
|
function waitFor(predicate) {
|
|
|
|
const time = setInterval(() => {
|
|
|
|
if (predicate()) {
|
|
|
|
clearInterval(time);
|
|
|
|
}
|
|
|
|
}, 20);
|
|
|
|
}
|
2024-11-21 17:35:27 +03:00
|
|
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
2014-11-20 22:51:24 +03:00
|
|
|
|
2023-11-26 23:32:26 +03:00
|
|
|
// Replace conversion with our own system and test: __TEST__ prefix must be used, otherwise
|
|
|
|
// other tests will interfere
|
|
|
|
let typeAtoB = 0, typeBtoC = 0, typeCtoA = 0, typeDtoA = 0, typeCtoE = 0;
|
|
|
|
//set all same costs to get easy testing, know which path will be taken
|
2024-02-04 20:48:25 +03:00
|
|
|
Convertor.learn(T_A, T_B, (tile, x) => {
|
2023-11-26 23:32:26 +03:00
|
|
|
typeAtoB++;
|
|
|
|
return x+1;
|
|
|
|
});
|
2024-11-21 17:35:27 +03:00
|
|
|
// Costly conversion to C simulation
|
|
|
|
Convertor.learn(T_B, T_C, async (tile, x) => {
|
2023-11-26 23:32:26 +03:00
|
|
|
typeBtoC++;
|
2024-11-21 17:35:27 +03:00
|
|
|
await sleep(5);
|
2023-11-26 23:32:26 +03:00
|
|
|
return x+1;
|
|
|
|
});
|
2024-02-04 20:48:25 +03:00
|
|
|
Convertor.learn(T_C, T_A, (tile, x) => {
|
2023-11-26 23:32:26 +03:00
|
|
|
typeCtoA++;
|
|
|
|
return x+1;
|
|
|
|
});
|
2024-02-04 20:48:25 +03:00
|
|
|
Convertor.learn(T_D, T_A, (tile, x) => {
|
2023-11-26 23:32:26 +03:00
|
|
|
typeDtoA++;
|
|
|
|
return x+1;
|
|
|
|
});
|
2024-02-04 20:48:25 +03:00
|
|
|
Convertor.learn(T_C, T_E, (tile, x) => {
|
2023-11-26 23:32:26 +03:00
|
|
|
typeCtoE++;
|
|
|
|
return x+1;
|
|
|
|
});
|
|
|
|
//'Copy constructors'
|
|
|
|
let copyA = 0, copyB = 0, copyC = 0, copyD = 0, copyE = 0;
|
|
|
|
//also learn destructors
|
2024-02-04 20:48:25 +03:00
|
|
|
Convertor.learn(T_A, T_A,(tile, x) => {
|
2023-11-26 23:32:26 +03:00
|
|
|
copyA++;
|
|
|
|
return x+1;
|
|
|
|
});
|
2024-02-04 20:48:25 +03:00
|
|
|
Convertor.learn(T_B, T_B,(tile, x) => {
|
2023-11-26 23:32:26 +03:00
|
|
|
copyB++;
|
|
|
|
return x+1;
|
|
|
|
});
|
2024-02-04 20:48:25 +03:00
|
|
|
Convertor.learn(T_C, T_C,(tile, x) => {
|
2023-11-26 23:32:26 +03:00
|
|
|
copyC++;
|
|
|
|
return x-1;
|
|
|
|
});
|
2024-02-04 20:48:25 +03:00
|
|
|
Convertor.learn(T_D, T_D,(tile, x) => {
|
2023-11-26 23:32:26 +03:00
|
|
|
copyD++;
|
|
|
|
return x+1;
|
|
|
|
});
|
2024-02-04 20:48:25 +03:00
|
|
|
Convertor.learn(T_E, T_E,(tile, x) => {
|
2023-11-26 23:32:26 +03:00
|
|
|
copyE++;
|
|
|
|
return x+1;
|
|
|
|
});
|
|
|
|
let destroyA = 0, destroyB = 0, destroyC = 0, destroyD = 0, destroyE = 0;
|
|
|
|
//also learn destructors
|
|
|
|
Convertor.learnDestroy(T_A, () => {
|
|
|
|
destroyA++;
|
|
|
|
});
|
|
|
|
Convertor.learnDestroy(T_B, () => {
|
|
|
|
destroyB++;
|
|
|
|
});
|
|
|
|
Convertor.learnDestroy(T_C, () => {
|
|
|
|
destroyC++;
|
|
|
|
});
|
|
|
|
Convertor.learnDestroy(T_D, () => {
|
|
|
|
destroyD++;
|
|
|
|
});
|
|
|
|
Convertor.learnDestroy(T_E, () => {
|
|
|
|
destroyE++;
|
|
|
|
});
|
|
|
|
|
2024-11-21 17:35:27 +03:00
|
|
|
OpenSeadragon.TestCacheDrawer = class extends OpenSeadragon.DrawerBase {
|
|
|
|
constructor(opts) {
|
|
|
|
super(opts);
|
|
|
|
this.testEvents = new OpenSeadragon.EventSource();
|
|
|
|
}
|
|
|
|
|
|
|
|
getType() {
|
|
|
|
return "test-cache-drawer";
|
|
|
|
}
|
|
|
|
|
|
|
|
// Make test use private cache
|
|
|
|
get defaultOptions() {
|
|
|
|
return {
|
|
|
|
usePrivateCache: true
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
getSupportedDataFormats() {
|
|
|
|
return [T_C, T_E];
|
|
|
|
}
|
|
|
|
|
|
|
|
static isSupported() {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
_createDrawingElement() {
|
|
|
|
return document.createElement("div");
|
|
|
|
}
|
|
|
|
|
|
|
|
draw(tiledImages) {
|
|
|
|
for (let image of tiledImages) {
|
|
|
|
const tilesDoDraw = image.getTilesToDraw().map(info => info.tile);
|
|
|
|
for (let tile of tilesDoDraw) {
|
|
|
|
const data = this.getDataToDraw(tile);
|
|
|
|
this.testEvents.raiseEvent('test-tile', {
|
|
|
|
tile: tile,
|
|
|
|
dataToDraw: data,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
canRotate() {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
destroy() {
|
|
|
|
//noop
|
|
|
|
}
|
|
|
|
|
|
|
|
setImageSmoothingEnabled(imageSmoothingEnabled){
|
|
|
|
//noop
|
|
|
|
}
|
|
|
|
|
|
|
|
drawDebuggingRect(rect) {
|
|
|
|
//noop
|
|
|
|
}
|
|
|
|
|
|
|
|
clear(){
|
|
|
|
//noop
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
OpenSeadragon.EmptyTestT_ATileSource = class extends OpenSeadragon.TileSource {
|
|
|
|
|
|
|
|
supports( data, url ){
|
|
|
|
return data && data.isTestSource;
|
|
|
|
}
|
|
|
|
|
|
|
|
configure( data, url, postData ){
|
|
|
|
return {
|
|
|
|
width: 512, /* width *required */
|
|
|
|
height: 512, /* height *required */
|
|
|
|
tileSize: 128, /* tileSize *required */
|
|
|
|
tileOverlap: 0, /* tileOverlap *required */
|
|
|
|
minLevel: 0, /* minLevel */
|
|
|
|
maxLevel: 3, /* maxLevel */
|
|
|
|
tilesUrl: "", /* tilesUrl */
|
|
|
|
fileFormat: "", /* fileFormat */
|
|
|
|
displayRects: null /* displayRects */
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
getTileUrl(level, x, y) {
|
|
|
|
return String(level); //treat each tile on level same to introduce cache overlaps
|
|
|
|
}
|
|
|
|
|
|
|
|
downloadTileStart(context) {
|
|
|
|
context.finish(0, null, T_A);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-11-20 22:51:24 +03:00
|
|
|
// ----------
|
2017-12-07 04:20:39 +03:00
|
|
|
QUnit.module('TileCache', {
|
|
|
|
beforeEach: function () {
|
2023-11-18 22:16:35 +03:00
|
|
|
$('<div id="example"></div>').appendTo("#qunit-fixture");
|
|
|
|
|
2014-11-20 22:51:24 +03:00
|
|
|
testLog.reset();
|
2023-11-18 22:16:35 +03:00
|
|
|
|
|
|
|
viewer = OpenSeadragon({
|
|
|
|
id: 'example',
|
|
|
|
prefixUrl: '/build/openseadragon/images/',
|
|
|
|
maxImageCacheCount: 200, //should be enough to fit test inside the cache
|
2024-11-21 17:35:27 +03:00
|
|
|
springStiffness: 100, // Faster animation = faster tests
|
|
|
|
drawer: 'test-cache-drawer',
|
2023-11-18 22:16:35 +03:00
|
|
|
});
|
|
|
|
OpenSeadragon.ImageLoader.prototype.addJob = originalJob;
|
2024-10-18 15:38:04 +03:00
|
|
|
|
|
|
|
// Reset counters
|
|
|
|
typeAtoB = 0, typeBtoC = 0, typeCtoA = 0, typeDtoA = 0, typeCtoE = 0;
|
|
|
|
copyA = 0, copyB = 0, copyC = 0, copyD = 0, copyE = 0;
|
|
|
|
destroyA = 0, destroyB = 0, destroyC = 0, destroyD = 0, destroyE = 0;
|
2014-11-20 22:51:24 +03:00
|
|
|
},
|
2017-12-07 04:20:39 +03:00
|
|
|
afterEach: function () {
|
2023-11-18 22:16:35 +03:00
|
|
|
if (viewer && viewer.close) {
|
|
|
|
viewer.close();
|
|
|
|
}
|
|
|
|
|
|
|
|
viewer = null;
|
2014-11-20 22:51:24 +03:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// ----------
|
2017-12-07 04:20:39 +03:00
|
|
|
QUnit.test('basics', function(assert) {
|
2023-11-26 23:32:26 +03:00
|
|
|
const done = assert.async();
|
2024-03-05 12:48:07 +03:00
|
|
|
const fakeViewer = MockSeadragon.getViewer(
|
|
|
|
MockSeadragon.getDrawer({
|
2024-02-05 11:42:26 +03:00
|
|
|
// tile in safe mode inspects the supported formats upon cache set
|
|
|
|
getSupportedDataFormats() {
|
|
|
|
return [T_A, T_B, T_C, T_D, T_E];
|
|
|
|
}
|
2024-03-05 12:48:07 +03:00
|
|
|
})
|
|
|
|
);
|
|
|
|
const fakeTiledImage0 = MockSeadragon.getTiledImage(fakeViewer);
|
|
|
|
const fakeTiledImage1 = MockSeadragon.getTiledImage(fakeViewer);
|
2014-11-20 22:51:24 +03:00
|
|
|
|
2024-03-05 12:48:07 +03:00
|
|
|
const tile0 = MockSeadragon.getTile('foo.jpg', fakeTiledImage0);
|
|
|
|
const tile1 = MockSeadragon.getTile('foo.jpg', fakeTiledImage1);
|
2014-11-20 22:51:24 +03:00
|
|
|
|
2023-11-26 23:32:26 +03:00
|
|
|
const cache = new OpenSeadragon.TileCache();
|
2017-12-07 04:20:39 +03:00
|
|
|
assert.equal(cache.numTilesLoaded(), 0, 'no tiles to begin with');
|
2014-11-20 22:51:24 +03:00
|
|
|
|
2023-11-26 23:32:26 +03:00
|
|
|
tile0._caches[tile0.cacheKey] = cache.cacheTile({
|
|
|
|
tile: tile0,
|
2024-10-18 15:38:04 +03:00
|
|
|
tiledImage: fakeTiledImage0,
|
|
|
|
data: 3,
|
|
|
|
dataType: T_A
|
2014-11-20 22:51:24 +03:00
|
|
|
});
|
2023-11-26 23:32:26 +03:00
|
|
|
tile0._cacheSize++;
|
2014-11-20 22:51:24 +03:00
|
|
|
|
2017-12-07 04:20:39 +03:00
|
|
|
assert.equal(cache.numTilesLoaded(), 1, 'tile count after cache');
|
2014-11-20 22:51:24 +03:00
|
|
|
|
2023-11-26 23:32:26 +03:00
|
|
|
tile1._caches[tile1.cacheKey] = cache.cacheTile({
|
|
|
|
tile: tile1,
|
2024-10-18 15:38:04 +03:00
|
|
|
tiledImage: fakeTiledImage1,
|
|
|
|
data: 55,
|
|
|
|
dataType: T_B
|
2014-11-20 22:51:24 +03:00
|
|
|
});
|
2023-11-26 23:32:26 +03:00
|
|
|
tile1._cacheSize++;
|
2017-12-07 04:20:39 +03:00
|
|
|
assert.equal(cache.numTilesLoaded(), 2, 'tile count after second cache');
|
2014-11-20 22:51:24 +03:00
|
|
|
|
|
|
|
cache.clearTilesFor(fakeTiledImage0);
|
|
|
|
|
2017-12-07 04:20:39 +03:00
|
|
|
assert.equal(cache.numTilesLoaded(), 1, 'tile count after first clear');
|
2014-11-20 22:51:24 +03:00
|
|
|
|
|
|
|
cache.clearTilesFor(fakeTiledImage1);
|
|
|
|
|
2017-12-07 04:20:39 +03:00
|
|
|
assert.equal(cache.numTilesLoaded(), 0, 'tile count after second clear');
|
2014-11-20 22:51:24 +03:00
|
|
|
|
2017-12-07 04:20:39 +03:00
|
|
|
done();
|
2014-11-20 22:51:24 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
// ----------
|
2017-12-07 04:20:39 +03:00
|
|
|
QUnit.test('maxImageCacheCount', function(assert) {
|
2023-11-26 23:32:26 +03:00
|
|
|
const done = assert.async();
|
2024-03-05 12:48:07 +03:00
|
|
|
const fakeViewer = MockSeadragon.getViewer(
|
|
|
|
MockSeadragon.getDrawer({
|
2024-02-05 11:42:26 +03:00
|
|
|
// tile in safe mode inspects the supported formats upon cache set
|
|
|
|
getSupportedDataFormats() {
|
|
|
|
return [T_A, T_B, T_C, T_D, T_E];
|
|
|
|
}
|
2024-03-05 12:48:07 +03:00
|
|
|
})
|
|
|
|
);
|
|
|
|
const fakeTiledImage0 = MockSeadragon.getTiledImage(fakeViewer);
|
|
|
|
const tile0 = MockSeadragon.getTile('different.jpg', fakeTiledImage0);
|
|
|
|
const tile1 = MockSeadragon.getTile('same.jpg', fakeTiledImage0);
|
|
|
|
const tile2 = MockSeadragon.getTile('same.jpg', fakeTiledImage0);
|
2014-11-20 22:51:24 +03:00
|
|
|
|
2023-11-26 23:32:26 +03:00
|
|
|
const cache = new OpenSeadragon.TileCache({
|
2014-11-20 22:51:24 +03:00
|
|
|
maxImageCacheCount: 1
|
|
|
|
});
|
|
|
|
|
2017-12-07 04:20:39 +03:00
|
|
|
assert.equal(cache.numTilesLoaded(), 0, 'no tiles to begin with');
|
2014-11-20 22:51:24 +03:00
|
|
|
|
2023-11-26 23:32:26 +03:00
|
|
|
tile0._caches[tile0.cacheKey] = cache.cacheTile({
|
|
|
|
tile: tile0,
|
2024-10-18 15:38:04 +03:00
|
|
|
tiledImage: fakeTiledImage0,
|
|
|
|
data: 55,
|
|
|
|
dataType: T_B
|
2014-11-20 22:51:24 +03:00
|
|
|
});
|
2023-11-26 23:32:26 +03:00
|
|
|
tile0._cacheSize++;
|
2014-11-20 22:51:24 +03:00
|
|
|
|
2017-12-07 04:20:39 +03:00
|
|
|
assert.equal(cache.numTilesLoaded(), 1, 'tile count after add');
|
2014-11-20 22:51:24 +03:00
|
|
|
|
2023-11-26 23:32:26 +03:00
|
|
|
tile1._caches[tile1.cacheKey] = cache.cacheTile({
|
|
|
|
tile: tile1,
|
2024-10-18 15:38:04 +03:00
|
|
|
tiledImage: fakeTiledImage0,
|
|
|
|
data: 55,
|
|
|
|
dataType: T_B
|
2014-11-20 22:51:24 +03:00
|
|
|
});
|
2023-11-26 23:32:26 +03:00
|
|
|
tile1._cacheSize++;
|
2014-11-20 22:51:24 +03:00
|
|
|
|
2017-12-07 04:20:39 +03:00
|
|
|
assert.equal(cache.numTilesLoaded(), 1, 'tile count after add of second image');
|
2014-11-20 22:51:24 +03:00
|
|
|
|
2023-11-26 23:32:26 +03:00
|
|
|
tile2._caches[tile2.cacheKey] = cache.cacheTile({
|
|
|
|
tile: tile2,
|
2024-10-18 15:38:04 +03:00
|
|
|
tiledImage: fakeTiledImage0,
|
|
|
|
data: 55,
|
|
|
|
dataType: T_B
|
2014-11-20 22:51:24 +03:00
|
|
|
});
|
2023-11-26 23:32:26 +03:00
|
|
|
tile2._cacheSize++;
|
2014-11-20 22:51:24 +03:00
|
|
|
|
2017-12-07 04:20:39 +03:00
|
|
|
assert.equal(cache.numTilesLoaded(), 2, 'tile count after additional same image');
|
2014-11-20 22:51:24 +03:00
|
|
|
|
2017-12-07 04:20:39 +03:00
|
|
|
done();
|
2014-11-20 22:51:24 +03:00
|
|
|
});
|
|
|
|
|
2023-11-26 23:32:26 +03:00
|
|
|
//Tile API and cache interaction
|
2024-11-21 17:35:27 +03:00
|
|
|
QUnit.test('Tile: basic rendering & test setup', function(test) {
|
2023-11-26 23:32:26 +03:00
|
|
|
const done = test.async();
|
|
|
|
|
2024-11-21 17:35:27 +03:00
|
|
|
const tileCache = viewer.tileCache;
|
|
|
|
const drawer = viewer.drawer;
|
|
|
|
|
|
|
|
let testTileCalled = false;
|
|
|
|
drawer.testEvents.addHandler('test-tile', e => {
|
|
|
|
testTileCalled = true;
|
|
|
|
test.ok(e.dataToDraw, "Tile data is ready to be drawn");
|
|
|
|
});
|
2023-11-26 23:32:26 +03:00
|
|
|
|
2024-11-21 17:35:27 +03:00
|
|
|
viewer.addHandler('open', async () => {
|
|
|
|
await viewer.waitForFinishedJobsForTest();
|
|
|
|
await sleep(1); // necessary to make space for a draw call
|
2023-11-26 23:32:26 +03:00
|
|
|
|
2024-11-21 17:35:27 +03:00
|
|
|
test.ok(viewer.world.getItemAt(0).source instanceof OpenSeadragon.EmptyTestT_ATileSource, "Tests are done with empty test source type T_A.");
|
|
|
|
test.ok(viewer.world.getItemAt(1).source instanceof OpenSeadragon.EmptyTestT_ATileSource, "Tests are done with empty test source type T_A.");
|
|
|
|
test.ok(testTileCalled, "Drawer tested at least one tile.");
|
2023-11-26 23:32:26 +03:00
|
|
|
|
2024-11-21 17:35:27 +03:00
|
|
|
test.ok(typeAtoB > 1, "At least one conversion was triggered.");
|
|
|
|
test.equal(typeAtoB, typeBtoC, "A->B = B->C, since we need to move all data to T_C for the drawer.");
|
2023-11-26 23:32:26 +03:00
|
|
|
|
2024-11-21 17:35:27 +03:00
|
|
|
for (let tile of tileCache._tilesLoaded) {
|
|
|
|
const cache = tile.getCache();
|
|
|
|
test.equal(cache.type, T_A, "Cache data was not affected, the drawer uses internal cache.");
|
|
|
|
|
|
|
|
const internalCache = cache.getDataForRendering(drawer, tile);
|
|
|
|
test.equal(internalCache.type, T_C, "Conversion A->C ready, since there is no way to get to T_E.");
|
|
|
|
test.ok(internalCache.loaded, "Internal cache ready.");
|
|
|
|
}
|
2024-10-17 13:10:04 +03:00
|
|
|
|
|
|
|
done();
|
2024-11-21 17:35:27 +03:00
|
|
|
});
|
|
|
|
viewer.open([
|
|
|
|
{isTestSource: true},
|
|
|
|
{isTestSource: true},
|
|
|
|
]);
|
2024-10-17 13:10:04 +03:00
|
|
|
});
|
|
|
|
|
2024-11-21 17:35:27 +03:00
|
|
|
QUnit.test('Tile & Invalidation API: basic conversion & preprocessing', function(test) {
|
2024-10-17 13:10:04 +03:00
|
|
|
const done = test.async();
|
|
|
|
|
2024-11-21 17:35:27 +03:00
|
|
|
const tileCache = viewer.tileCache;
|
|
|
|
const drawer = viewer.drawer;
|
2024-10-17 13:10:04 +03:00
|
|
|
|
2024-11-21 17:35:27 +03:00
|
|
|
let testTileCalled = false;
|
|
|
|
|
|
|
|
let _currentTestVal = undefined;
|
|
|
|
let previousTestValue = undefined;
|
|
|
|
drawer.testEvents.addHandler('test-tile', e => {
|
|
|
|
test.ok(e.dataToDraw, "Tile data is ready to be drawn");
|
|
|
|
if (_currentTestVal !== undefined) {
|
|
|
|
testTileCalled = true;
|
|
|
|
test.equal(e.dataToDraw, _currentTestVal, "Value is correct on the drawn data.");
|
|
|
|
}
|
|
|
|
});
|
2024-10-17 13:10:04 +03:00
|
|
|
|
2024-11-21 17:35:27 +03:00
|
|
|
function testDrawingRoutine(value) {
|
|
|
|
_currentTestVal = value;
|
|
|
|
viewer.world.needsDraw();
|
|
|
|
viewer.world.draw();
|
|
|
|
previousTestValue = value;
|
|
|
|
_currentTestVal = undefined;
|
|
|
|
}
|
2024-10-17 13:10:04 +03:00
|
|
|
|
2024-11-21 17:35:27 +03:00
|
|
|
viewer.addHandler('open', async () => {
|
|
|
|
await viewer.waitForFinishedJobsForTest();
|
|
|
|
await sleep(1); // necessary to make space for a draw call
|
2024-10-17 13:10:04 +03:00
|
|
|
|
2024-11-21 17:35:27 +03:00
|
|
|
// Test simple data set -> creates main cache
|
2024-10-17 13:10:04 +03:00
|
|
|
|
2024-11-21 17:35:27 +03:00
|
|
|
let testHandler = async e => {
|
|
|
|
// data comes in as T_A
|
|
|
|
test.equal(typeDtoA, 0, "No conversion needed to get type A.");
|
|
|
|
test.equal(typeCtoA, 0, "No conversion needed to get type A.");
|
2024-10-17 13:10:04 +03:00
|
|
|
|
2024-11-21 17:35:27 +03:00
|
|
|
const data = await e.getData(T_A);
|
|
|
|
test.equal(data, 1, "Copy: creation of a working cache.");
|
|
|
|
e.tile.__TEST_PROCESSED = true;
|
2024-10-17 13:10:04 +03:00
|
|
|
|
2024-11-21 17:35:27 +03:00
|
|
|
// Test value 2 since we set T_C no need to convert
|
|
|
|
await e.setData(2, T_C);
|
|
|
|
test.notOk(e.outdated(), "Event is still valid.");
|
|
|
|
};
|
2024-10-17 13:10:04 +03:00
|
|
|
|
2024-11-21 17:35:27 +03:00
|
|
|
viewer.addHandler('tile-invalidated', testHandler);
|
|
|
|
await viewer.world.requestInvalidate(true);
|
|
|
|
await sleep(1); // necessary to make space for internal updates
|
|
|
|
testDrawingRoutine(2);
|
2024-10-17 13:10:04 +03:00
|
|
|
|
2024-11-21 17:35:27 +03:00
|
|
|
//test for each level only single cache was processed
|
|
|
|
const processedLevels = {};
|
|
|
|
for (let tile of tileCache._tilesLoaded) {
|
|
|
|
const level = tile.level;
|
2024-10-17 13:10:04 +03:00
|
|
|
|
2024-11-21 17:35:27 +03:00
|
|
|
if (tile.__TEST_PROCESSED) {
|
|
|
|
test.ok(!processedLevels[level], "Only single tile processed per level.");
|
|
|
|
processedLevels[level] = true;
|
|
|
|
delete tile.__TEST_PROCESSED;
|
|
|
|
}
|
2024-10-17 13:10:04 +03:00
|
|
|
|
2024-11-21 17:35:27 +03:00
|
|
|
const origCache = tile.getCache(tile.originalCacheKey);
|
|
|
|
test.equal(origCache.type, T_A, "Original cache data was not affected, the drawer uses internal cache.");
|
|
|
|
test.equal(origCache.data, 0, "Original cache data was not affected, the drawer uses internal cache.");
|
2024-10-17 13:10:04 +03:00
|
|
|
|
2024-11-21 17:35:27 +03:00
|
|
|
const cache = tile.getCache();
|
|
|
|
test.equal(cache.type, T_C, "Main Cache Updated (suite 1)");
|
|
|
|
test.equal(cache.data, previousTestValue, "Main Cache Updated (suite 1)");
|
2024-10-17 13:10:04 +03:00
|
|
|
|
2024-11-21 17:35:27 +03:00
|
|
|
const internalCache = cache.getDataForRendering(drawer, tile);
|
|
|
|
test.equal(T_C, internalCache.type, "Conversion A->C ready, since there is no way to get to T_E.");
|
|
|
|
test.ok(internalCache.loaded, "Internal cache ready.");
|
|
|
|
}
|
|
|
|
|
|
|
|
// Test that basic scenario with reset data false starts from the main cache data of previous round
|
|
|
|
const modificationConstant = 50;
|
|
|
|
viewer.removeHandler('tile-invalidated', testHandler);
|
|
|
|
testHandler = async e => {
|
|
|
|
const data = await e.getData(T_B);
|
|
|
|
test.equal(data, previousTestValue + 2, "C -> A -> B conversion happened.");
|
|
|
|
await e.setData(data + modificationConstant, T_B);
|
|
|
|
console.log(data + modificationConstant);
|
|
|
|
test.notOk(e.outdated(), "Event is still valid.");
|
|
|
|
};
|
|
|
|
console.log(previousTestValue, modificationConstant)
|
|
|
|
|
|
|
|
viewer.addHandler('tile-invalidated', testHandler);
|
|
|
|
await viewer.world.requestInvalidate(false);
|
|
|
|
await sleep(1); // necessary to make space for a draw call
|
|
|
|
// We set data as TB - there is T_C -> T_A -> T_B -> T_C conversion round
|
|
|
|
let newValue = previousTestValue + modificationConstant + 3;
|
|
|
|
testDrawingRoutine(newValue);
|
|
|
|
|
|
|
|
newValue--; // intenrla cache performed +1 conversion, but here we have main cache with one step less
|
|
|
|
for (let tile of tileCache._tilesLoaded) {
|
|
|
|
const cache = tile.getCache();
|
|
|
|
test.equal(cache.type, T_B, "Main Cache Updated (suite 2).");
|
|
|
|
test.equal(cache.data, newValue, "Main Cache Updated (suite 2).");
|
|
|
|
}
|
2023-11-26 23:32:26 +03:00
|
|
|
|
2024-11-21 17:35:27 +03:00
|
|
|
// Now test whether data reset works, value 1 -> copy perfomed due to internal cache cration
|
|
|
|
viewer.removeHandler('tile-invalidated', testHandler);
|
|
|
|
testHandler = async e => {
|
|
|
|
const data = await e.getData(T_B);
|
|
|
|
test.equal(data, 1, "Copy: creation of a working cache.");
|
|
|
|
await e.setData(-8, T_E);
|
|
|
|
e.resetData();
|
|
|
|
};
|
|
|
|
viewer.addHandler('tile-invalidated', testHandler);
|
|
|
|
await viewer.world.requestInvalidate(true);
|
|
|
|
await sleep(1); // necessary to make space for a draw call
|
|
|
|
testDrawingRoutine(2); // Value +2 rendering from original data
|
|
|
|
|
|
|
|
for (let tile of tileCache._tilesLoaded) {
|
|
|
|
const origCache = tile.getCache(tile.originalCacheKey);
|
|
|
|
test.ok(tile.getCache() === origCache, "Main cache is now original cache.");
|
|
|
|
}
|
|
|
|
|
|
|
|
// Now force main cache creation that differs
|
|
|
|
viewer.removeHandler('tile-invalidated', testHandler);
|
|
|
|
testHandler = async e => {
|
|
|
|
await e.setData(41, T_B);
|
|
|
|
};
|
|
|
|
viewer.addHandler('tile-invalidated', testHandler);
|
|
|
|
await viewer.world.requestInvalidate(true);
|
|
|
|
|
|
|
|
// Now test whether data reset works, even with non-original data
|
|
|
|
viewer.removeHandler('tile-invalidated', testHandler);
|
|
|
|
testHandler = async e => {
|
|
|
|
const data = await e.getData(T_B);
|
|
|
|
test.equal(data, 42, "Copy: 41 + 1.");
|
|
|
|
await e.setData(data, T_E);
|
|
|
|
e.resetData();
|
|
|
|
};
|
|
|
|
viewer.addHandler('tile-invalidated', testHandler);
|
|
|
|
await viewer.world.requestInvalidate(false);
|
|
|
|
await sleep(1); // necessary to make space for a draw call
|
|
|
|
testDrawingRoutine(42);
|
|
|
|
|
|
|
|
for (let tile of tileCache._tilesLoaded) {
|
|
|
|
const origCache = tile.getCache(tile.originalCacheKey);
|
|
|
|
test.equal(origCache.type, T_A, "Original cache data was not affected, the drawer uses main cache even after refresh.");
|
|
|
|
test.equal(origCache.data, 0, "Original cache data was not affected, the drawer uses main cache even after refresh.");
|
|
|
|
}
|
|
|
|
|
|
|
|
test.ok(testTileCalled, "Drawer tested at least one tile.");
|
2023-11-26 23:32:26 +03:00
|
|
|
done();
|
2024-11-21 17:35:27 +03:00
|
|
|
});
|
|
|
|
viewer.open([
|
|
|
|
{isTestSource: true},
|
|
|
|
{isTestSource: true},
|
|
|
|
]);
|
2023-11-26 23:32:26 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
//Tile API and cache interaction
|
|
|
|
QUnit.test('Tile API Cache Interaction', function(test) {
|
|
|
|
const done = test.async();
|
2024-03-05 12:48:07 +03:00
|
|
|
const fakeViewer = MockSeadragon.getViewer(
|
|
|
|
MockSeadragon.getDrawer({
|
2024-02-05 11:42:26 +03:00
|
|
|
// tile in safe mode inspects the supported formats upon cache set
|
|
|
|
getSupportedDataFormats() {
|
|
|
|
return [T_A, T_B, T_C, T_D, T_E];
|
|
|
|
}
|
2024-03-05 12:48:07 +03:00
|
|
|
})
|
|
|
|
);
|
|
|
|
const tileCache = fakeViewer.tileCache;
|
|
|
|
const fakeTiledImage0 = MockSeadragon.getTiledImage(fakeViewer);
|
|
|
|
const fakeTiledImage1 = MockSeadragon.getTiledImage(fakeViewer);
|
2023-11-26 23:32:26 +03:00
|
|
|
|
|
|
|
//load data
|
2024-03-05 12:48:07 +03:00
|
|
|
const tile00 = MockSeadragon.getTile('foo.jpg', fakeTiledImage0);
|
2024-03-03 16:50:01 +03:00
|
|
|
tile00.addCache(tile00.cacheKey, 0, T_A, false, false);
|
2024-03-05 12:48:07 +03:00
|
|
|
const tile01 = MockSeadragon.getTile('foo2.jpg', fakeTiledImage0);
|
2024-03-03 16:50:01 +03:00
|
|
|
tile01.addCache(tile01.cacheKey, 0, T_B, false, false);
|
2024-03-05 12:48:07 +03:00
|
|
|
const tile10 = MockSeadragon.getTile('foo3.jpg', fakeTiledImage1);
|
2024-03-03 16:50:01 +03:00
|
|
|
tile10.addCache(tile10.cacheKey, 0, T_C, false, false);
|
2024-03-05 12:48:07 +03:00
|
|
|
const tile11 = MockSeadragon.getTile('foo3.jpg', fakeTiledImage1);
|
2024-03-03 16:50:01 +03:00
|
|
|
tile11.addCache(tile11.cacheKey, 0, T_C, false, false);
|
2024-03-05 12:48:07 +03:00
|
|
|
const tile12 = MockSeadragon.getTile('foo.jpg', fakeTiledImage1);
|
2024-03-03 16:50:01 +03:00
|
|
|
tile12.addCache(tile12.cacheKey, 0, T_A, false, false);
|
2023-11-26 23:32:26 +03:00
|
|
|
|
|
|
|
//test set/get data in async env
|
|
|
|
(async function() {
|
2024-10-17 13:10:04 +03:00
|
|
|
test.equal(tileCache.numTilesLoaded(), 5, "We loaded 5 tiles");
|
|
|
|
test.equal(tileCache.numCachesLoaded(), 3, "We loaded 3 cache objects - three different urls");
|
|
|
|
|
|
|
|
const c00 = tile00.getCache(tile00.cacheKey);
|
|
|
|
const c12 = tile12.getCache(tile12.cacheKey);
|
|
|
|
|
|
|
|
//now test multi-cache within tile
|
|
|
|
const theTileKey = tile00.cacheKey;
|
2024-11-21 17:35:27 +03:00
|
|
|
tile00.addCache(tile00.buildDistinctMainCacheKey(), 42, T_E, true, false);
|
2024-10-17 13:10:04 +03:00
|
|
|
test.notEqual(tile00.cacheKey, tile00.originalCacheKey, "New cache rendered.");
|
|
|
|
|
|
|
|
//now add artifically another record
|
|
|
|
tile00.addCache("my_custom_cache", 128, T_C);
|
|
|
|
test.equal(tileCache.numTilesLoaded(), 5, "We still loaded only 5 tiles.");
|
|
|
|
test.equal(tileCache.numCachesLoaded(), 5, "The cache has now 5 items (two new added already).");
|
|
|
|
|
|
|
|
test.equal(c00.getTileCount(), 2, "The cache still has only two tiles attached.");
|
|
|
|
test.equal(tile00.getCacheSize(), 3, "The tile has three cache objects (original data, main cache & custom.");
|
|
|
|
//related tile not affected
|
2024-11-21 17:35:27 +03:00
|
|
|
test.equal(tile12.cacheKey, tile12.originalCacheKey, "Original cache change not reflected on shared caches.");
|
2024-10-17 13:10:04 +03:00
|
|
|
test.equal(tile12.originalCacheKey, theTileKey, "Original cache key also preserved.");
|
|
|
|
test.equal(c12.getTileCount(), 2, "The original data cache still has only two tiles attached.");
|
|
|
|
|
2024-10-18 15:38:04 +03:00
|
|
|
//add and delete cache nothing changes (+1 destroy T_C)
|
|
|
|
tile00.addCache("my_custom_cache2", 128, T_C);
|
|
|
|
tile00.removeCache("my_custom_cache2");
|
|
|
|
test.equal(tileCache.numTilesLoaded(), 5, "We still loaded only 5 tiles.");
|
|
|
|
test.equal(tileCache.numCachesLoaded(), 5, "The cache has now 5 items.");
|
|
|
|
test.equal(tile00.getCacheSize(), 3, "The tile has three cache objects.");
|
|
|
|
|
|
|
|
//delete cache as a zombie (+0 destroy)
|
|
|
|
tile00.addCache("my_custom_cache2", 17, T_D);
|
|
|
|
//direct access shoes correct value although we set key!
|
|
|
|
const myCustomCache2Data = tile00.getCache("my_custom_cache2").data;
|
|
|
|
test.equal(myCustomCache2Data, 17, "Previously defined cache does not intervene.");
|
|
|
|
test.equal(tileCache.numCachesLoaded(), 6, "The cache size is 6.");
|
|
|
|
//keep zombie
|
|
|
|
tile00.removeCache("my_custom_cache2", false);
|
|
|
|
test.equal(tileCache.numCachesLoaded(), 6, "The cache is 5 + 1 zombie, no change.");
|
|
|
|
test.equal(tile00.getCacheSize(), 3, "The tile has three cache objects.");
|
|
|
|
test.equal(tileCache._zombiesLoadedCount, 1, "One zombie.");
|
|
|
|
|
|
|
|
//revive zombie
|
|
|
|
tile01.addCache("my_custom_cache2", 18, T_D);
|
|
|
|
const myCustomCache2OtherData = tile01.getCache("my_custom_cache2").data;
|
|
|
|
test.equal(myCustomCache2OtherData, myCustomCache2Data, "Caches are equal because revived.");
|
|
|
|
test.equal(tileCache._cachesLoadedCount, 6, "Zombie revived, original state restored.");
|
|
|
|
test.equal(tileCache._zombiesLoadedCount, 0, "No zombies.");
|
|
|
|
|
|
|
|
//again, keep zombie
|
|
|
|
tile01.removeCache("my_custom_cache2", false);
|
|
|
|
|
|
|
|
//first create additional cache so zombie is not the youngest
|
|
|
|
tile01.addCache("some weird cache", 11, T_A);
|
|
|
|
test.ok(tile01.cacheKey === tile01.originalCacheKey, "Custom cache does not touch tile cache keys.");
|
|
|
|
|
|
|
|
//insertion aadditional cache clears the zombie first although it is not the youngest one
|
|
|
|
test.equal(tileCache.numCachesLoaded(), 7, "The cache has now 7 items.");
|
|
|
|
test.equal(tileCache._cachesLoadedCount, 6, "New cache created -> 5+1.");
|
|
|
|
test.equal(tileCache._zombiesLoadedCount, 1, "One zombie remains.");
|
|
|
|
|
|
|
|
//Test CAP
|
|
|
|
tileCache._maxCacheItemCount = 7;
|
|
|
|
|
|
|
|
// Zombie destroyed before other caches (+1 destroy T_D)
|
2024-11-21 17:35:27 +03:00
|
|
|
tile12.addCache("someKey", 43, T_B);
|
2024-10-18 15:38:04 +03:00
|
|
|
test.equal(tileCache.numCachesLoaded(), 7, "The cache has now 7 items.");
|
|
|
|
test.equal(tileCache._zombiesLoadedCount, 0, "One zombie sacrificed, preferred over living cache.");
|
|
|
|
test.notOk([tile00, tile01, tile10, tile11, tile12].find(x => !x.loaded), "All tiles sill loaded since zombie was sacrificed.");
|
|
|
|
|
|
|
|
// test destructors called as expected
|
|
|
|
test.equal(destroyA, 0, "No destructors for A called.");
|
|
|
|
test.equal(destroyB, 0, "No destructors for B called.");
|
|
|
|
test.equal(destroyC, 1, "One destruction for C called.");
|
|
|
|
test.equal(destroyD, 1, "One destruction for D called.");
|
|
|
|
test.equal(destroyE, 0, "No destructors for E called.");
|
|
|
|
|
|
|
|
|
|
|
|
//try to revive zombie will fail: the zombie was deleted, we will find new vaue there
|
|
|
|
tile01.addCache("my_custom_cache2", -849613, T_C);
|
|
|
|
const myCustomCache2RecreatedData = tile01.getCache("my_custom_cache2").data;
|
|
|
|
test.notEqual(myCustomCache2RecreatedData, myCustomCache2Data, "Caches are not equal because zombie was killed.");
|
|
|
|
test.equal(myCustomCache2RecreatedData, -849613, "Cache data is actually as set to 18.");
|
|
|
|
test.equal(tileCache.numCachesLoaded(), 7, "The cache has still 7 items.");
|
|
|
|
|
|
|
|
// some tile has been selected as a sacrifice since we triggered cap control
|
|
|
|
test.ok([tile00, tile01, tile10, tile11, tile12].find(x => !x.loaded), "One tile has been sacrificed.");
|
2024-10-17 13:10:04 +03:00
|
|
|
done();
|
2023-11-26 23:32:26 +03:00
|
|
|
})();
|
|
|
|
});
|
|
|
|
|
2023-11-18 22:16:35 +03:00
|
|
|
QUnit.test('Zombie Cache', function(test) {
|
|
|
|
const done = test.async();
|
|
|
|
|
2024-10-17 13:10:04 +03:00
|
|
|
//test jobs by coverage: fail if cached coverage not fully re-stored without jobs
|
|
|
|
let jobCounter = 0, coverage = undefined;
|
|
|
|
OpenSeadragon.ImageLoader.prototype.addJob = function (options) {
|
|
|
|
jobCounter++;
|
|
|
|
if (coverage) {
|
|
|
|
//old coverage of previous tiled image: if loaded, fail --> should be in cache
|
|
|
|
const coverageItem = coverage[options.tile.level][options.tile.x][options.tile.y];
|
|
|
|
test.ok(!coverageItem, "Attempt to add job for tile that should be already in memory.");
|
|
|
|
}
|
|
|
|
return originalJob.call(this, options);
|
|
|
|
};
|
|
|
|
|
|
|
|
let tilesFinished = 0;
|
|
|
|
const tileCounter = function (event) {tilesFinished++;}
|
|
|
|
|
|
|
|
const openHandler = function(event) {
|
|
|
|
event.item.allowZombieCache(true);
|
|
|
|
|
|
|
|
viewer.world.removeHandler('add-item', openHandler);
|
|
|
|
test.ok(jobCounter === 0, 'Initial state, no images loaded');
|
|
|
|
|
|
|
|
waitFor(() => {
|
|
|
|
if (tilesFinished === jobCounter && event.item._fullyLoaded) {
|
|
|
|
coverage = $.extend(true, {}, event.item.coverage);
|
|
|
|
viewer.world.removeAll();
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
let jobsAfterRemoval = 0;
|
|
|
|
const removalHandler = function (event) {
|
|
|
|
viewer.world.removeHandler('remove-item', removalHandler);
|
|
|
|
test.ok(jobCounter > 0, 'Tiled image removed after 100 ms, should load some images.');
|
|
|
|
jobsAfterRemoval = jobCounter;
|
|
|
|
|
|
|
|
viewer.world.addHandler('add-item', reopenHandler);
|
|
|
|
viewer.addTiledImage({
|
|
|
|
tileSource: '/test/data/testpattern.dzi'
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
const reopenHandler = function (event) {
|
|
|
|
event.item.allowZombieCache(true);
|
|
|
|
|
|
|
|
viewer.removeHandler('add-item', reopenHandler);
|
|
|
|
test.equal(jobCounter, jobsAfterRemoval, 'Reopening image does not fetch any tiles imemdiatelly.');
|
|
|
|
|
|
|
|
waitFor(() => {
|
|
|
|
if (event.item._fullyLoaded) {
|
|
|
|
viewer.removeHandler('tile-unloaded', unloadTileHandler);
|
|
|
|
viewer.removeHandler('tile-loaded', tileCounter);
|
|
|
|
coverage = undefined;
|
|
|
|
|
|
|
|
//console test needs here explicit removal to finish correctly
|
|
|
|
OpenSeadragon.ImageLoader.prototype.addJob = originalJob;
|
|
|
|
done();
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
const unloadTileHandler = function (event) {
|
|
|
|
test.equal(event.destroyed, false, "Tile unload event should not delete with zombies!");
|
|
|
|
}
|
|
|
|
|
|
|
|
viewer.world.addHandler('add-item', openHandler);
|
|
|
|
viewer.world.addHandler('remove-item', removalHandler);
|
|
|
|
viewer.addHandler('tile-unloaded', unloadTileHandler);
|
|
|
|
viewer.addHandler('tile-loaded', tileCounter);
|
|
|
|
|
|
|
|
viewer.open('/test/data/testpattern.dzi');
|
2023-11-18 22:16:35 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
QUnit.test('Zombie Cache Replace Item', function(test) {
|
|
|
|
const done = test.async();
|
|
|
|
|
2024-10-17 13:10:04 +03:00
|
|
|
let jobCounter = 0, coverage = undefined;
|
|
|
|
OpenSeadragon.ImageLoader.prototype.addJob = function (options) {
|
|
|
|
jobCounter++;
|
|
|
|
if (coverage) {
|
|
|
|
//old coverage of previous tiled image: if loaded, fail --> should be in cache
|
|
|
|
const coverageItem = coverage[options.tile.level][options.tile.x][options.tile.y];
|
|
|
|
if (!coverageItem) {
|
|
|
|
console.warn(coverage, coverage[options.tile.level][options.tile.x], options.tile);
|
|
|
|
}
|
|
|
|
test.ok(!coverageItem, "Attempt to add job for tile data that was previously loaded.");
|
|
|
|
}
|
|
|
|
return originalJob.call(this, options);
|
|
|
|
};
|
|
|
|
|
|
|
|
let tilesFinished = 0;
|
|
|
|
const tileCounter = function (event) {tilesFinished++;}
|
|
|
|
|
|
|
|
const openHandler = function(event) {
|
|
|
|
event.item.allowZombieCache(true);
|
|
|
|
viewer.world.removeHandler('add-item', openHandler);
|
|
|
|
viewer.world.addHandler('add-item', reopenHandler);
|
|
|
|
|
|
|
|
waitFor(() => {
|
|
|
|
if (tilesFinished === jobCounter && event.item._fullyLoaded) {
|
|
|
|
coverage = $.extend(true, {}, event.item.coverage);
|
|
|
|
viewer.addTiledImage({
|
|
|
|
tileSource: '/test/data/testpattern.dzi',
|
|
|
|
index: 0,
|
|
|
|
replace: true
|
|
|
|
});
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
const reopenHandler = function (event) {
|
|
|
|
event.item.allowZombieCache(true);
|
|
|
|
|
|
|
|
viewer.removeHandler('add-item', reopenHandler);
|
|
|
|
waitFor(() => {
|
|
|
|
if (event.item._fullyLoaded) {
|
|
|
|
viewer.removeHandler('tile-unloaded', unloadTileHandler);
|
|
|
|
viewer.removeHandler('tile-loaded', tileCounter);
|
|
|
|
|
|
|
|
//console test needs here explicit removal to finish correctly
|
|
|
|
OpenSeadragon.ImageLoader.prototype.addJob = originalJob;
|
|
|
|
done();
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
const unloadTileHandler = function (event) {
|
|
|
|
test.equal(event.destroyed, false, "Tile unload event should not delete with zombies!");
|
|
|
|
}
|
|
|
|
|
|
|
|
viewer.world.addHandler('add-item', openHandler);
|
|
|
|
viewer.addHandler('tile-unloaded', unloadTileHandler);
|
|
|
|
viewer.addHandler('tile-loaded', tileCounter);
|
|
|
|
|
|
|
|
viewer.open('/test/data/testpattern.dzi');
|
2023-11-18 22:16:35 +03:00
|
|
|
});
|
|
|
|
|
2014-11-20 22:51:24 +03:00
|
|
|
})();
|