Skip to content

Commit

Permalink
Use the quad_info endpoint for tile_frames.
Browse files Browse the repository at this point in the history
Now, precaching the tile frames can be done by calling the
tile_frames/quad_info endpoint with cache=schedule.  To match what is
done in the javascript side, this endpoint needs to be called per
channel with frameBase=<channel>, frameStride=<number of channels>,
frameGroup=<IndexZ || 1>, frameGroupStride=<IndexZ / IndexC>.  This is
in addition to the defaults used in this project defined in
src/store/index.ts around line 745.

To make this more convenient, we could add (to large_image) a special
value for frameBase which would schedule all tile_frames based on this
heuristic.
  • Loading branch information
manthey committed Feb 25, 2022
1 parent 307cd0a commit ecd77c5
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 195 deletions.
8 changes: 4 additions & 4 deletions src/components/ImageViewer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -441,14 +441,14 @@ export default class ImageViewer extends Vue {
if (!fullLayer.setFrameQuad) {
setFrameQuad(someImage.tileinfo, fullLayer, {
...baseQuadOptions,
progress: () => {
if (this.cacheProgress < this.cacheProgressTotal) {
progress: (status) => {
if (status.loadedCount === 0) {
this.cacheProgressTotal += status.images.length;
} else if (this.cacheProgress < this.cacheProgressTotal) {
this.cacheProgress += 1;
}
}
});
this.cacheProgressTotal +=
fullLayer.setFrameQuad.status.images.length;
}
fullLayer.setFrameQuad(singleFrame);
}
Expand Down
2 changes: 2 additions & 0 deletions src/store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -740,6 +740,8 @@ export class Main extends VuexModule {
if (results.fullUrls && results.fullUrls.length && results.fullUrls[0]) {
results.baseQuadOptions = {
baseUrl: results.fullUrls[0].split("/tiles")[0] + "/tiles",
restRequest: (params) => this.api.client.get(params.url, {params: params.data}).then((data) => data.data),
restUrl: 'item/' + results.fullUrls[0].split("/tiles")[0].split('item/')[1] + "/tiles",
maxTextures: 32,
maxTextureSize: 4096,
query:
Expand Down
297 changes: 106 additions & 191 deletions src/utils/setFrameQuad.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
* minimally include ``baseUrl``.
* @param {string} options.baseUrl The reference to the tile endpoint, e.g.,
* <url>/api/v1/item/<item id>/tiles.
* @param {string} options.restRequest A backbone-like ajax handler function.
* @param {string} options.restUrl A reference to the tile endpoint as used by
* the restRequest function, e.g., item/<item id>/tiles.
* @param {string} [options.format='encoding=JPEG&jpegQuality=85&jpegSubsampling=1']
* The compression and format for the texture.
* @param {string} [options.query] Additional query options to add to the
Expand Down Expand Up @@ -55,6 +58,8 @@
* crossOrigin policy for images.
* @param {string} [options.progress] If specified, a function to call whenever
* a texture image is loaded.
* @param {boolean} [options.redrawOnFirstLoad=true] If truthy, redraw the
* layer after the base quad is first loaded if a frame value has been set.
*/
function setFrameQuad(tileinfo, layer, options) {
layer.setFrameQuad = function() {};
Expand All @@ -73,217 +78,127 @@ function setFrameQuad(tileinfo, layer, options) {
layer.renderer()._maxTextureSize ||
layer.renderer().constructor._maxTextureSize;
} catch (err) {}
const w = tileinfo.sizeX,
h = tileinfo.sizeY,
maxTotalPixels = options.maxTotalTexturePixels || 1073741824,
alignment = options.alignment || 16;
let numFrames = (tileinfo.frames || []).length || 1,
texSize = maxTextureSize || 8192,
textures = options.maxTextures || 1;
const frames = [];
for (let fds = 0; fds < (options.frameGroupStride || 1); fds += 1) {
for (
let fidx = (options.frameBase || 0) + fds * (options.frameStride || 1);
fidx < numFrames;
fidx += (options.frameStride || 1) * (options.frameGroupStride || 1)
) {
frames.push(fidx);
}
}
numFrames = frames.length;
if (numFrames === 0 || !Object.getOwnPropertyDescriptor(layer, "baseQuad")) {
return;
}
texSize = Math.min(texSize, options.maxTextureSize || texSize);
while (texSize ** 2 > maxTotalPixels) {
texSize /= 2;
}
while (textures && texSize ** 2 * textures > maxTotalPixels) {
textures -= 1;
}
let fw, fh, fhorz, fvert, fperframe;
/* Iterate in case we can reduce the number of textures or the texture
* size */
while (true) {
let f = Math.ceil(numFrames / textures); // frames per texture
if ((options.frameGroup || 1) > 1) {
const fg = Math.ceil(f / options.frameGroup) * options.frameGroup;
if (fg / f <= (options.frameGroupFactor || 4)) {
f = fg;
}
}
const texScale2 = texSize ** 2 / f / w / h;
// frames across the texture
fhorz = Math.ceil(
texSize / (Math.ceil((w * texScale2 ** 0.5) / alignment) * alignment)
);
fvert = Math.ceil(
texSize / (Math.ceil((h * texScale2 ** 0.5) / alignment) * alignment)
);
// tile sizes
fw = Math.floor(texSize / fhorz / alignment) * alignment;
fvert = Math.max(Math.ceil(f / Math.floor(texSize / fw)), fvert);
fh = Math.floor(texSize / fvert / alignment) * alignment;
if (options.maxFrameSize) {
const maxFrameSize =
Math.floor(options.maxFrameSize / alignment) * alignment;
fw = Math.min(fw, maxFrameSize);
fh = Math.min(fh, maxFrameSize);
}
if (fw > w) {
fw = Math.ceil(w / alignment) * alignment;
}
if (fh > h) {
fh = Math.ceil(h / alignment) * alignment;
}
// shrink one dimension to account for aspect ratio
fw = Math.min(Math.ceil((fh * w) / h / alignment) * alignment, fw);
fh = Math.min(Math.ceil((fw * h) / w / alignment) * alignment, fh);
// recompute frames across the texture
fhorz = Math.floor(texSize / fw);
fvert = Math.min(Math.floor(texSize / fh), Math.ceil(numFrames / fhorz));
fperframe = fhorz * fvert;
if (textures > 1 && (options.frameGroup || 1) > 1) {
fperframe =
Math.floor(fperframe / options.frameGroup) * options.frameGroup;
if (
textures * fperframe < numFrames &&
fhorz * fvert * textures >= numFrames
) {
fperframe = fhorz * fvert;
}
}
// check if we are not using all textures or are using less than a
// quarter of one texture. If not, stop, if so, reduce and recalculate
if (textures > 1 && numFrames <= fperframe * (textures - 1)) {
textures -= 1;
continue;
}
if (
fhorz >= 2 &&
Math.ceil(f / Math.floor(fhorz / 2)) * fh <= texSize / 2
) {
texSize /= 2;
continue;
}
break;
}
// used area of each tile
const usedw = Math.floor(w / Math.max(w / fw, h / fh)),
usedh = Math.floor(h / Math.max(w / fw, h / fh));
// get the set of texture images
options = Object.assign(
{},
{ maxTextureSize: Math.min(16384, maxTextureSize) },
options
);
const status = {
tileinfo: tileinfo,
options: options,
images: [],
src: [],
quads: [],
frames: frames,
frames: ["placeholder"],
framesToIdx: {},
loadedCount: 0
};
if (tileinfo.tileWidth && tileinfo.tileHeight) {
// report that tiles below this level are not needed
status.minLevel = Math.ceil(
Math.log(
Math.min(usedw / tileinfo.tileWidth, usedh / tileinfo.tileHeight)
) / Math.log(2)
);
}
frames.forEach((frame, idx) => {
status.framesToIdx[frame] = idx;
});
for (let idx = 0; idx < textures; idx += 1) {
const img = new Image();
if (
options.baseUrl.indexOf(":") >= 0 &&
options.baseUrl.indexOf("/") === options.baseUrl.indexOf(":") + 1
) {
img.crossOrigin = options.crossOrigin || "anonymous";
}
const frameList = frames.slice(idx * fperframe, (idx + 1) * fperframe);
let src = `${options.baseUrl}/tile_frames?framesAcross=${fhorz}&width=${fw}&height=${fh}&fill=corner:black&exact=false`;
if (frameList.length !== (tileinfo.frames || []).length) {
src += `&frameList=${frameList.join(",")}`;
}
src +=
"&" +
(
options.format || "encoding=JPEG&jpegQuality=85&jpegSubsampling=1"
).replace(/(^&|^\?|\?$|&$)/g, "");
if (options.query) {
src += "&" + options.query.replace(/(^&|^\?|\?$|&$)/g, "");
}
status.src.push(src);
if (idx === textures - 1) {
img.onload = function() {
status.loadedCount += 1;
status.loaded = true;
let qiOptions = Object.assign({}, options);
[
"restRequest",
"restUrl",
"baseUrl",
"crossOrigin",
"progress",
"redrawOnFirstLoad"
].forEach(k => delete qiOptions[k]);
options
.restRequest({
type: "GET",
url: `${options.restUrl}/tile_frames/quad_info`,
data: qiOptions
})
.then(data => {
status.quads = data.quads;
status.frames = data.frames;
status.framesToIdx = data.framesToIdx;
for (let idx = 0; idx < data.src.length; idx += 1) {
const img = new Image();
for (let qidx = 0; qidx < data.quads.length; qidx += 1) {
if (data.quadsToIdx[qidx] === idx) {
status.quads[qidx].image = img;
}
}
if (
layer._options &&
layer._options.minLevel !== undefined &&
(options.adjustMinLevel === undefined || options.adjustMinLevel) &&
status.minLevel &&
status.minLevel > layer._options.minLevel
options.baseUrl.indexOf(":") >= 0 &&
options.baseUrl.indexOf("/") === options.baseUrl.indexOf(":") + 1
) {
layer._options.minLevel = Math.min(
layer._options.maxLevel,
status.minLevel
);
img.crossOrigin = options.crossOrigin || "anonymous";
}
if (options.progress) {
try {
options.progress(status);
} catch (err) {}
let params = Object.keys(data.src[idx])
.map(
k =>
encodeURIComponent(k) + "=" + encodeURIComponent(data.src[idx][k])
)
.join("&");
let src = `${options.baseUrl}/tile_frames?` + params;
status.src.push(src);
if (idx === data.src.length - 1) {
img.onload = function() {
status.loadedCount += 1;
status.loaded = true;
if (
layer._options &&
layer._options.minLevel !== undefined &&
(options.adjustMinLevel === undefined ||
options.adjustMinLevel) &&
status.minLevel &&
status.minLevel > layer._options.minLevel
) {
layer._options.minLevel = Math.min(
layer._options.maxLevel,
status.minLevel
);
}
if (options.progress) {
try {
options.progress(status);
} catch (err) {}
}
if (status.frame !== undefined) {
layer.baseQuad = Object.assign(
{},
status.quads[status.framesToIdx[status.frame]]
);
if (
options.redrawOnFirstLoad ||
options.redrawOnFirstLoad === undefined
) {
layer.draw();
}
}
};
} else {
(idx => {
img.onload = function() {
status.loadedCount += 1;
status.images[idx + 1].src = status.src[idx + 1];
if (options.progress) {
try {
options.progress(status);
} catch (err) {}
}
};
})(idx);
}
};
} else {
(idx => {
img.onload = function() {
status.loadedCount += 1;
status.images[idx + 1].src = status.src[idx + 1];
if (options.progress) {
try {
options.progress(status);
} catch (err) {}
}
};
})(idx);
}
status.images.push(img);
// the last image can have fewer frames than the other images
const f = frameList.length;
const ivert = Math.ceil(f / fhorz),
ihorz = Math.min(f, fhorz);
frameList.forEach((frame, fidx) => {
const quad = {
// z = -1 to place under other tile layers
ul: { x: 0, y: 0, z: -1 },
// y coordinate is inverted
lr: { x: w, y: -h, z: -1 },
crop: {
x: w,
y: h,
left: (fidx % ihorz) * fw,
top: (ivert - Math.floor(fidx / ihorz)) * fh - usedh,
right: (fidx % ihorz) * fw + usedw,
bottom: (ivert - Math.floor(fidx / ihorz)) * fh
},
image: img
};
status.quads.push(quad);
status.images.push(img);
}
status.images[0].src = status.src[0];
if (options.progress) {
try {
options.progress(status);
} catch (err) {}
}
return status;
});
}
status.images[0].src = status.src[0];

layer.setFrameQuad = function(frame) {
if (status.framesToIdx[frame] !== undefined) {
if (status.framesToIdx[frame] !== undefined && status.loaded) {
layer.baseQuad = Object.assign(
{},
status.quads[status.framesToIdx[frame]]
);
status.frame = frame;
}
status.frame = frame;
};
layer.setFrameQuad.status = status;
}
Expand Down

0 comments on commit ecd77c5

Please sign in to comment.