Merge pull request #9430 from lutzhelm/iiif

Adds IIIF Image API tile source
This commit is contained in:
Tim Schaub
2019-05-10 06:32:28 -06:00
committed by GitHub
10 changed files with 1657 additions and 0 deletions

View File

@@ -220,3 +220,11 @@ Duplicate item added to a unique collection. For example, it may be that you tr
### 59
Invalid command found in the PBF. This indicates that the loaded vector tile may be corrupt.
### 60
Missing or invalid `size`.
### 61
Cannot determine IIIF Image API version from provided image information JSON.

16
examples/iiif.html Normal file
View File

@@ -0,0 +1,16 @@
---
layout: example.html
title: IIIF Image API
shortdesc: Example of a IIIF Image API source.
docs: >
Example of a tile source for an International Image Interoperability Framework (IIIF) Image Service.
Try any Image API version 1 or 2 service.
tags: "IIIF, IIIF Image API, tile source"
---
<div id="map" class="map"></div>
<div class="controls">
<div id="iiif-notification">&nbsp;</div>
Enter <code>info.json</code> URL:
<input type="text" id="imageInfoUrl" value="https://iiif.ub.uni-leipzig.de/iiif/j2k/0000/0107/0000010732/00000072.jpx/info.json">
<button id="display">Display image</button>
</div>

46
examples/iiif.js Normal file
View File

@@ -0,0 +1,46 @@
import Map from '../src/ol/Map.js';
import View from '../src/ol/View.js';
import TileLayer from '../src/ol/layer/Tile.js';
import IIIF from '../src/ol/source/IIIF.js';
import IIIFInfo from '../src/ol/format/IIIFInfo.js';
const layer = new TileLayer(),
map = new Map({
layers: [layer],
target: 'map'
}),
notifyDiv = document.getElementById('iiif-notification'),
urlInput = document.getElementById('imageInfoUrl'),
displayButton = document.getElementById('display');
function refreshMap(imageInfoUrl) {
fetch(imageInfoUrl).then(function(response) {
response.json().then(function(imageInfo) {
const options = new IIIFInfo(imageInfo).getTileSourceOptions();
if (options === undefined || options.version === undefined) {
notifyDiv.textContent = 'Data seems to be no valid IIIF image information.';
return;
}
options.zDirection = -1;
const iiifTileSource = new IIIF(options);
layer.setSource(iiifTileSource);
map.setView(new View({
resolutions: iiifTileSource.getTileGrid().getResolutions(),
extent: iiifTileSource.getTileGrid().getExtent(),
constrainOnlyCenter: true
}));
map.getView().fit(iiifTileSource.getTileGrid().getExtent());
notifyDiv.textContent = '';
}).catch(function(body) {
notifyDiv.textContent = 'Could not read image info json. ' + body;
});
}).catch(function() {
notifyDiv.textContent = 'Could not read data from URL.';
});
}
displayButton.addEventListener('click', function() {
refreshMap(urlInput.value);
});
refreshMap(urlInput.value);

View File

@@ -7,6 +7,7 @@ export {default as GeoJSON} from './format/GeoJSON.js';
export {default as GML} from './format/GML.js';
export {default as GPX} from './format/GPX.js';
export {default as IGC} from './format/IGC.js';
export {default as IIIFInfo} from './format/IIIFInfo.js';
export {default as KML} from './format/KML.js';
export {default as MVT} from './format/MVT.js';
export {default as OWS} from './format/OWS.js';

426
src/ol/format/IIIFInfo.js Normal file
View File

@@ -0,0 +1,426 @@
/**
* @module ol/format/IIIFInfo
*/
import {assert} from '../asserts.js';
/**
* @typedef {Object} PreferredOptions
* @property {string} [format] Preferred image format. Will be used if the image information
* indicates support for that format.
* @property {string} [quality] IIIF image qualitiy. Will be used if the image information
* indicates support for that quality.
*/
/**
* @typedef {Object} SupportedFeatures
* @property {Array<string>} [supports] Supported IIIF image size and region
* calculation features.
* @property {Array<string>} [formats] Supported image formats.
* @property {Array<string>} [qualities] Supported IIIF image qualities.
*/
/**
* @typedef {Object} ImageInformationResponse1_0
* @property {string} identifier
* @property {number} width
* @property {number} height
* @property {Array<number>} [scale_factors] Resolution scaling factors.
* @property {number} [tile_width]
* @property {number} [tile_height]
* @property {Array<string>} [formats] Supported image formats.
* @property {string} [profile] Compliance level URI.
*/
/**
* @typedef {Object} ImageInformationResponse1_1
* @property {string} "@id" The base URI of the image service.
* @property {string} "@context" JSON-LD context URI.
* @property {number} width Full image width.
* @property {number} height Full image height.
* @property {Array<number>} [scale_factors] Resolution scaling factors.
* @property {number} [tile_width]
* @property {number} [tile_height]
* @property {Array<string>} [formats] Supported image formats.
* @property {string} [profile] Compliance level URI.
*/
/**
* @typedef {Object} TileInfo
* @property {Array<number>} scaleFactors Supported resolution scaling factors.
* @property {number} width Tile width in pixels.
* @property {number} [height] Tile height in pixels. Same as tile width if height is
* not given.
*/
/**
* @typedef {Object} IiifProfile
* @property {Array<string>} [formats] Supported image formats for the image service.
* @property {Array<string>} [qualities] Supported IIIF image qualities.
* @property {Array<string>} [supports] Supported features.
* @property {number} [maxArea] Maximum area (pixels) available for this image service.
* @property {number} [maxHeight] Maximum height.
* @property {number} [maxWidth] Maximum width.
*/
/**
* @typedef {Object} ImageInformationResponse2
* @property {string} "@id" The base URI of the image service.
* @property {string} "@context" JSON-LD context IRI
* @property {number} width Full image width.
* @property {number} height Full image height.
* @property {Array<string|IiifProfile>} profile Additional informations about the image
* service's capabilities.
* @property {Array<Object<string, number>>} [sizes] Supported full image dimensions.
* @property {Array<TileInfo>} [tiles] Supported tile sizes and resolution scaling factors.
*/
/**
* @typedef {Object} ImageInformationResponse3
* @property {string} id The base URI of the image service.
* @property {string} "@context" JSON-LD context IRI
* @property {number} width Full image width.
* @property {number} height Full image height.
* @property {string} profile Compliance level, one of 'level0', 'level1' or 'level2'
* @property {Array<Object<string, number>>} [sizes] Supported full image dimensions.
* @property {Array<TileInfo>} [tiles] Supported tile sizes and resolution scaling factors.
* @property {number} [maxArea] Maximum area (pixels) available for this image service.
* @property {number} [maxHeight] Maximum height.
* @property {number} [maxWidth] Maximum width.
* @property {Array<string>} [extraQualities] IIIF image qualities supported by the
* image service additional to the ones indicated by the compliance level.
* @property {Array<string>} [extraFormats] Image formats supported by the
* image service additional to the ones indicated by the compliance level.
* @property {Array<string>} [extraFeatures] Additional supported features whose support
* is not indicated by the compliance level.
*/
/**
* @enum {string}
*/
export const Versions = {
VERSION1: 'version1',
VERSION2: 'version2',
VERSION3: 'version3'
};
/**
* Supported image formats, qualities and supported region / size calculation features
* for different image API versions and compliance levels
* @const
* @type {Object<string, Object<string, SupportedFeatures>>}
*/
const IIIF_PROFILE_VALUES = {};
IIIF_PROFILE_VALUES[Versions.VERSION1] = {
'level0': {
supports: [],
formats: [],
qualities: ['native']
},
'level1': {
supports: ['regionByPx', 'sizeByW', 'sizeByH', 'sizeByPct'],
formats: ['jpg'],
qualities: ['native']
},
'level2': {
supports: ['regionByPx', 'regionByPct', 'sizeByW', 'sizeByH', 'sizeByPct',
'sizeByConfinedWh', 'sizeByWh'],
formats: ['jpg', 'png'],
qualities: ['native', 'color', 'grey', 'bitonal']
}
};
IIIF_PROFILE_VALUES[Versions.VERSION2] = {
'level0': {
supports: [],
formats: ['jpg'],
qualities: ['default']
},
'level1': {
supports: ['regionByPx', 'sizeByW', 'sizeByH', 'sizeByPct'],
formats: ['jpg'],
qualities: ['default']
},
'level2': {
supports: ['regionByPx', 'regionByPct', 'sizeByW', 'sizeByH', 'sizeByPct',
'sizeByConfinedWh', 'sizeByDistortedWh', 'sizeByWh'],
formats: ['jpg', 'png'],
qualities: ['default', 'bitonal']
}
};
IIIF_PROFILE_VALUES[Versions.VERSION3] = {
'level0': {
supports: [],
formats: ['jpg'],
qualities: ['default']
},
'level1': {
supports: ['regionByPx', 'regionSquare', 'sizeByW', 'sizeByH'],
formats: ['jpg'],
qualities: ['default']
},
'level2': {
supports: ['regionByPx', 'regionSquare', 'regionByPct',
'sizeByW', 'sizeByH', 'sizeByPct', 'sizeByConfinedWh', 'sizeByWh'],
formats: ['jpg'],
qualities: ['default', 'bitonal']
}
};
IIIF_PROFILE_VALUES['none'] = {
'none': {
supports: [],
formats: [],
qualities: []
}
};
const COMPLIANCE_VERSION1 = new RegExp('^https?\:\/\/library\.stanford\.edu\/iiif\/image-api\/(1\.1\/)?compliance\.html#level[0-2]$');
const COMPLIANCE_VERSION2 = new RegExp('^https?\:\/\/iiif\.io\/api\/image\/2\/level[0-2](\.json)?$');
const COMPLIANCE_VERSION3 = new RegExp('(^https?\:\/\/iiif\.io\/api\/image\/3\/level[0-2](\.json)?$)|(^level[0-2]$)');
function generateVersion1Options(iiifInfo) {
let levelProfile = iiifInfo.getComplianceLevelSupportedFeatures();
// Version 1.0 and 1.1 do not require a profile.
if (levelProfile === undefined) {
levelProfile = IIIF_PROFILE_VALUES[Versions.VERSION1]['level0'];
}
return {
url: iiifInfo.imageInfo['@id'] === undefined ? undefined : iiifInfo.imageInfo['@id'].replace(/\/?(info.json)?$/g, ''),
supports: levelProfile.supports,
formats: [...levelProfile.formats, iiifInfo.imageInfo.formats === undefined ?
[] : iiifInfo.imageInfo.formats
],
qualities: [...levelProfile.qualities, iiifInfo.imageInfo.qualities === undefined ?
[] : iiifInfo.imageInfo.qualities
],
resolutions: iiifInfo.imageInfo.scale_factors,
tileSize: iiifInfo.imageInfo.tile_width !== undefined ? (iiifInfo.imageInfo.tile_height !== undefined ?
[iiifInfo.imageInfo.tile_width, iiifInfo.imageInfo.tile_height] : [iiifInfo.imageInfo.tile_width, iiifInfo.imageInfo.tile_width]) :
(iiifInfo.imageInfo.tile_height != undefined ? [iiifInfo.imageInfo.tile_height, iiifInfo.imageInfo.tile_height] : undefined)
};
}
function generateVersion2Options(iiifInfo) {
const levelProfile = iiifInfo.getComplianceLevelSupportedFeatures(),
additionalProfile = Array.isArray(iiifInfo.imageInfo.profile) && iiifInfo.imageInfo.profile.length > 1,
profileSupports = additionalProfile && iiifInfo.imageInfo.profile[1].supports ? iiifInfo.imageInfo.profile[1].supports : [],
profileFormats = additionalProfile && iiifInfo.imageInfo.profile[1].formats ? iiifInfo.imageInfo.profile[1].formats : [],
profileQualities = additionalProfile && iiifInfo.imageInfo.profile[1].qualities ? iiifInfo.imageInfo.profile[1].qualities : [];
return {
url: iiifInfo.imageInfo['@id'].replace(/\/?(info.json)?$/g, ''),
sizes: iiifInfo.imageInfo.sizes === undefined ? undefined : iiifInfo.imageInfo.sizes.map(function(size) {
return [size.width, size.height];
}),
tileSize: iiifInfo.imageInfo.tiles === undefined ? undefined : [
iiifInfo.imageInfo.tiles.map(function(tile) {
return tile.width;
})[0],
iiifInfo.imageInfo.tiles.map(function(tile) {
return tile.height === undefined ? tile.width : tile.height;
})[0]
],
resolutions: iiifInfo.imageInfo.tiles === undefined ? undefined :
iiifInfo.imageInfo.tiles.map(function(tile) {
return tile.scaleFactors;
})[0],
supports: [...levelProfile.supports, ...profileSupports],
formats: [...levelProfile.formats, ...profileFormats],
qualities: [...levelProfile.qualities, ...profileQualities]
};
}
function generateVersion3Options(iiifInfo) {
const levelProfile = iiifInfo.getComplianceLevelSupportedFeatures();
return {
url: iiifInfo.imageInfo['id'],
sizes: iiifInfo.imageInfo.sizes === undefined ? undefined : iiifInfo.imageInfo.sizes.map(function(size) {
return [size.width, size.height];
}),
tileSize: iiifInfo.imageInfo.tiles === undefined ? undefined : [
iiifInfo.imageInfo.tiles.map(function(tile) {
return tile.width;
})[0],
iiifInfo.imageInfo.tiles.map(function(tile) {
return tile.height;
})[0]
],
resolutions: iiifInfo.imageInfo.tiles === undefined ? undefined :
iiifInfo.imageInfo.tiles.map(function(tile) {
return tile.scaleFactors;
})[0],
supports: iiifInfo.imageInfo.extraFeatures === undefined ? levelProfile.supports :
[...levelProfile.supports, ...iiifInfo.imageInfo.extraFeatures],
formats: iiifInfo.imageInfo.extraFormats === undefined ? levelProfile.formats :
[...levelProfile.formats, ...iiifInfo.imageInfo.extraFormats],
qualities: iiifInfo.imageInfo.extraQualities === undefined ? levelProfile.qualities :
[...levelProfile.supports, ...iiifInfo.imageInfo.extraQualities],
maxWidth: undefined,
maxHeight: undefined,
maxArea: undefined
};
}
const versionFunctions = {};
versionFunctions[Versions.VERSION1] = generateVersion1Options;
versionFunctions[Versions.VERSION2] = generateVersion2Options;
versionFunctions[Versions.VERSION3] = generateVersion3Options;
/**
* @classdesc
* Format for transforming IIIF Image API image information responses into
* IIIF tile source ready options
*
* @api
*/
class IIIFInfo {
/**
* @param {ImageInformationResponse1_0|ImageInformationResponse1_1|ImageInformationResponse2|ImageInformationResponse3|string} imageInfo
* Deserialized image information JSON response object or JSON response as string
*/
constructor(imageInfo) {
this.setImageInfo(imageInfo);
}
/**
* @param {Object|string} imageInfo Deserialized image information JSON response
* object or JSON response as string
*/
setImageInfo(imageInfo) {
if (typeof imageInfo == 'string') {
this.imageInfo = JSON.parse(imageInfo);
} else {
this.imageInfo = imageInfo;
}
}
/**
* @returns {Versions} Major IIIF version.
*/
getImageApiVersion() {
if (this.imageInfo === undefined) {
return;
}
let context = this.imageInfo['@context'] || 'ol-no-context';
if (typeof context == 'string') {
context = [context];
}
for (let i = 0; i < context.length; i++) {
switch (context[i]) {
case 'http://library.stanford.edu/iiif/image-api/1.1/context.json':
case 'http://iiif.io/api/image/1/context.json':
return Versions.VERSION1;
case 'http://iiif.io/api/image/2/context.json':
return Versions.VERSION2;
case 'http://iiif.io/api/image/3/context.json':
return Versions.VERSION3;
case 'ol-no-context':
// Image API 1.0 has no '@context'
if (this.getComplianceLevelEntryFromProfile(Versions.VERSION1) && this.imageInfo.identifier) {
return Versions.VERSION1;
}
break;
default:
}
}
assert(false, 61);
}
/**
* @param {Versions} version Optional IIIF image API version
* @returns {string} Compliance level as it appears in the IIIF image information
* response.
*/
getComplianceLevelEntryFromProfile(version) {
if (this.imageInfo === undefined || this.imageInfo.profile === undefined) {
return;
}
if (version === undefined) {
version = this.getImageApiVersion();
}
switch (version) {
case Versions.VERSION1:
if (COMPLIANCE_VERSION1.test(this.imageInfo.profile)) {
return this.imageInfo.profile;
}
break;
case Versions.VERSION3:
if (COMPLIANCE_VERSION3.test(this.imageInfo.profile)) {
return this.imageInfo.profile;
}
break;
case Versions.VERSION2:
if (typeof this.imageInfo.profile === 'string' && COMPLIANCE_VERSION2.test(this.imageInfo.profile)) {
return this.imageInfo.profile;
}
if (Array.isArray(this.imageInfo.profile) && this.imageInfo.profile.length > 0
&& typeof this.imageInfo.profile[0] === 'string' && COMPLIANCE_VERSION2.test(this.imageInfo.profile[0])) {
return this.imageInfo.profile[0];
}
break;
default:
}
}
/**
* @param {Versions} version Optional IIIF image API version
* @returns {string} Compliance level, on of 'level0', 'level1' or 'level2' or undefined
*/
getComplianceLevelFromProfile(version) {
const complianceLevel = this.getComplianceLevelEntryFromProfile(version);
if (complianceLevel === undefined) {
return undefined;
}
const level = complianceLevel.match(/level[0-2](\.json)?$/g);
return Array.isArray(level) ? level[0].replace('.json', '') : undefined;
}
/**
* @returns {SupportedFeatures} Image formats, qualities and region / size calculation
* methods that are supported by the IIIF service.
*/
getComplianceLevelSupportedFeatures() {
if (this.imageInfo === undefined) {
return;
}
const version = this.getImageApiVersion();
const level = this.getComplianceLevelFromProfile(version);
if (level === undefined) {
return IIIF_PROFILE_VALUES['none']['none'];
}
return IIIF_PROFILE_VALUES[version][level];
}
/**
* @param {PreferredOptions} opt_preferredOptions Optional options for preferred format and quality.
* @returns {import("../source/IIIF.js").Options} IIIF tile source ready constructor options.
*/
getTileSourceOptions(opt_preferredOptions) {
const options = opt_preferredOptions || {},
version = this.getImageApiVersion();
if (version === undefined) {
return;
}
const imageOptions = version === undefined ? undefined : versionFunctions[version](this);
if (imageOptions === undefined) {
return;
}
return {
url: imageOptions.url,
version: version,
size: [this.imageInfo.width, this.imageInfo.height],
sizes: imageOptions.sizes,
format: imageOptions.formats.includes(options.format) ? options.format : 'jpg',
supports: imageOptions.supports,
quality: options.quality && imageOptions.qualities.includes(options.quality) ?
options.quality : imageOptions.qualities.includes('native') ? 'native' : 'default',
resolutions: Array.isArray(imageOptions.resolutions) ? imageOptions.resolutions.sort(function(a, b) {
return b - a;
}) : undefined,
tileSize: imageOptions.tileSize
};
}
}
export default IIIFInfo;

View File

@@ -5,6 +5,7 @@
export {default as BingMaps} from './source/BingMaps.js';
export {default as CartoDB} from './source/CartoDB.js';
export {default as Cluster} from './source/Cluster.js';
export {default as IIIF} from './source/IIIF.js';
export {default as Image} from './source/Image.js';
export {default as ImageArcGISRest} from './source/ImageArcGISRest.js';
export {default as ImageCanvas} from './source/ImageCanvas.js';

284
src/ol/source/IIIF.js Normal file
View File

@@ -0,0 +1,284 @@
/**
* @module ol/source/IIIF
*/
import {DEFAULT_TILE_SIZE} from '../tilegrid/common.js';
import {getTopLeft} from '../extent.js';
import {CustomTile} from './Zoomify.js';
import {Versions} from '../format/IIIFInfo.js';
import {assert} from '../asserts.js';
import TileGrid from '../tilegrid/TileGrid.js';
import TileImage from './TileImage.js';
/**
* @typedef {Object} Options
* @property {import("./Source.js").AttributionLike} [attributions] Attributions.
* @property {boolean} [attributionsCollapsible=true] Attributions are collapsible.
* @property {number} [cacheSize]
* @property {null|string} [crossOrigin]
* @property {import("../extent.js").Extent} [extent=[0, -height, width, 0]]
* @property {string} [format='jpg'] Requested image format.
* @property {import("../proj.js").ProjectionLike} [projection]
* @property {string} [quality] Requested IIIF image quality. Default is 'native'
* for version 1, 'default' for versions 2 and 3.
* @property {number} [reprojectionErrorThreshold=0.5] Maximum allowed reprojection error (in pixels).
* Higher values can increase reprojection performance, but decrease precision.
* @property {import("../size.js").Size} size Size of the image [width, height].
* @property {import("../size.js").Size[]} [sizes] Supported scaled image sizes.
* Content of the IIIF info.json 'sizes' property, but as array of Size objects.
* @property {import("./State.js").default} [state] Source state.
* @property {Array<string>} [supports=[]] Supported IIIF region and size calculation
* features.
* @property {number} [tilePixelRatio]
* @property {number|import("../size.js").Size} [tileSize] Tile size.
* Same tile size is used for all zoom levels. If tile size is a number,
* a square tile is assumed. If the IIIF image service supports arbitrary
* tiling (sizeByH, sizeByW, sizeByWh or sizeByPct as well as regionByPx or regionByPct
* are supported), the default tilesize is 256.
* @property {number} [transition]
* @property {string} [url] Base URL of the IIIF Image service.
* This should be the same as the IIIF Image @id.
* @property {Versions} [version=Versions.VERSION2] Service's IIIF Image API version.
* @property {number} [zDirection] Indicate which resolution should be used
* by a renderer if the views resolution does not match any resolution of the tile source.
* If 0, the nearest resolution will be used. If 1, the nearest lower resolution
* will be used. If -1, the nearest higher resolution will be used.
*/
function formatPercentage(percentage) {
return percentage.toLocaleString('en', {maximumFractionDigits: 10});
}
/**
* @classdesc
* Layer source for IIIF Image API services.
* @api
*/
class IIIF extends TileImage {
constructor(opt_options) {
const options = opt_options || {};
let baseUrl = options.url || '';
baseUrl = baseUrl + (baseUrl.lastIndexOf('/') === baseUrl.length - 1 || baseUrl === '' ? '' : '/');
const version = options.version || Versions.VERSION2;
const sizes = options.sizes || [];
const size = options.size;
assert(size != undefined && Array.isArray(size) && size.length == 2 &&
!isNaN(size[0]) && size[0] > 0 && !isNaN(size[1]) && size[1] > 0, 60);
const width = size[0];
const height = size[1];
const tileSize = options.tileSize;
const format = options.format || 'jpg';
const quality = options.quality || (options.version == Versions.VERSION1 ? 'native' : 'default');
let resolutions = options.resolutions || [];
const supports = options.supports || [];
const extent = options.extent || [0, -height, width, 0];
const supportsListedSizes = sizes != undefined && Array.isArray(sizes) && sizes.length > 0;
const supportsListedTiles = tileSize != undefined && (Number.isInteger(tileSize) && tileSize > 0 || Array.isArray(tileSize) && tileSize.length > 0);
const supportsArbitraryTiling = supports != undefined && Array.isArray(supports) &&
(supports.includes('regionByPx') || supports.includes('regionByPct')) &&
(supports.includes('sizeByWh') || supports.includes('sizeByH') ||
supports.includes('sizeByW') || supports.includes('sizeByPct'));
let tileWidth,
tileHeight,
maxZoom;
resolutions.sort(function(a, b) {
return b - a;
});
if (supportsListedTiles || supportsArbitraryTiling) {
if (tileSize != undefined) {
if (Number.isInteger(tileSize) && tileSize > 0) {
tileWidth = tileSize;
tileHeight = tileSize;
} else if (Array.isArray(tileSize) && tileSize.length > 0) {
if (tileSize.length == 1 || tileSize[1] == undefined && Number.isInteger(tileSize[0])) {
tileWidth = tileSize[0];
tileHeight = tileSize[0];
}
if (tileSize.length == 2) {
if (Number.isInteger(tileSize[0]) && Number.isInteger(tileSize[1])) {
tileWidth = tileSize[0];
tileHeight = tileSize[1];
} else if (tileSize[0] == undefined && Number.isInteger(tileSize[1])) {
tileWidth = tileSize[1];
tileHeight = tileSize[1];
}
}
}
}
if (tileWidth === undefined || tileHeight === undefined) {
tileWidth = DEFAULT_TILE_SIZE;
tileHeight = DEFAULT_TILE_SIZE;
}
if (resolutions.length == 0) {
maxZoom = Math.max(
Math.ceil(Math.log(width / tileWidth) / Math.LN2),
Math.ceil(Math.log(height / tileHeight) / Math.LN2)
);
for (let i = maxZoom; i >= 0; i--) {
resolutions.push(Math.pow(2, i));
}
} else {
const maxScaleFactor = Math.max([...resolutions]);
// TODO maxScaleFactor might not be a power to 2
maxZoom = Math.round(Math.log(maxScaleFactor) / Math.LN2);
}
} else {
// No tile support.
tileWidth = width;
tileHeight = height;
resolutions = [];
if (supportsListedSizes) {
/*
* 'sizes' provided. Use full region in different resolutions. Every
* resolution has only one tile.
*/
sizes.sort(function(a, b) {
return a[0] - b[0];
});
maxZoom = -1;
const ignoredSizesIndex = [];
for (let i = 0; i < sizes.length; i++) {
const resolution = width / sizes[i][0];
if (resolutions.length > 0 && resolutions[resolutions.length - 1] == resolution) {
ignoredSizesIndex.push(i);
continue;
}
resolutions.push(resolution);
maxZoom++;
}
if (ignoredSizesIndex.length > 0) {
for (let i = 0; i < ignoredSizesIndex.length; i++) {
sizes.splice(ignoredSizesIndex[i] - i, 1);
}
}
} else {
// No useful image information at all. Try pseudo tile with full image.
resolutions.push(1);
sizes.push([width, height]);
maxZoom = 0;
}
}
const tileGrid = new TileGrid({
tileSize: [tileWidth, tileHeight],
extent: extent,
origin: getTopLeft(extent),
resolutions: resolutions
});
const tileUrlFunction = function(tileCoord, pixelRatio, projection) {
let regionParam,
sizeParam;
const zoom = tileCoord[0];
if (zoom > maxZoom) {
return;
}
const tileX = tileCoord[1],
tileY = tileCoord[2],
scale = resolutions[zoom];
if (tileX === undefined || tileY === undefined || scale === undefined ||
tileX < 0 || Math.ceil(width / scale / tileWidth) <= tileX ||
tileY < 0 || Math.ceil(height / scale / tileHeight) <= tileY) {
return;
}
if (supportsArbitraryTiling || supportsListedTiles) {
const regionX = tileX * tileWidth * scale,
regionY = tileY * tileHeight * scale;
let regionW = tileWidth * scale,
regionH = tileHeight * scale,
sizeW = tileWidth,
sizeH = tileHeight;
if (regionX + regionW > width) {
regionW = width - regionX;
}
if (regionY + regionH > height) {
regionH = height - regionY;
}
if (regionX + tileWidth * scale > width) {
sizeW = Math.floor((width - regionX + scale - 1) / scale);
}
if (regionY + tileHeight * scale > height) {
sizeH = Math.floor((height - regionY + scale - 1) / scale);
}
if (regionX == 0 && regionW == width && regionY == 0 && regionH == height) {
// canonical full image region parameter is 'full', not 'x,y,w,h'
regionParam = 'full';
} else if (!supportsArbitraryTiling || supports.includes('regionByPx')) {
regionParam = regionX + ',' + regionY + ',' + regionW + ',' + regionH;
} else if (supports.includes('regionByPct')) {
const pctX = formatPercentage(regionX / width * 100),
pctY = formatPercentage(regionY / height * 100),
pctW = formatPercentage(regionW / width * 100),
pctH = formatPercentage(regionH / height * 100);
regionParam = 'pct:' + pctX + ',' + pctY + ',' + pctW + ',' + pctH;
}
if (version == Versions.VERSION3 && (!supportsArbitraryTiling || supports.includes('sizeByWh'))) {
sizeParam = sizeW + ',' + sizeH;
} else if (!supportsArbitraryTiling || supports.includes('sizeByW')) {
sizeParam = sizeW + ',';
} else if (supports.includes('sizeByH')) {
sizeParam = ',' + sizeH;
} else if (supports.includes('sizeByWh')) {
sizeParam = sizeW + ',' + sizeH;
} else if (supports.includes('sizeByPct')) {
sizeParam = 'pct:' + formatPercentage(100 / scale);
}
} else {
regionParam = 'full';
if (supportsListedSizes) {
const regionWidth = sizes[zoom][0],
regionHeight = sizes[zoom][1];
if (version == Versions.VERSION3) {
if (regionWidth == width && regionHeight == height) {
sizeParam = 'max';
} else {
sizeParam = regionWidth + ',' + regionHeight;
}
} else {
if (regionWidth == width) {
sizeParam = 'full';
} else {
sizeParam = regionWidth + ',';
}
}
} else {
sizeParam = version == Versions.VERSION3 ? 'max' : 'full';
}
}
return baseUrl + regionParam + '/' + sizeParam + '/0/' + quality + '.' + format;
};
const IiifTileClass = CustomTile.bind(null, tileGrid);
super({
attributions: options.attributions,
attributionsCollapsible: options.attributionsCollapsible,
cacheSize: options.cacheSize,
crossOrigin: options.crossOrigin,
projection: options.projection,
reprojectionErrorThreshold: options.reprojectionErrorThreshold,
state: options.state,
tileClass: IiifTileClass,
tileGrid: tileGrid,
tilePixelRatio: options.tilePixelRatio,
tileUrlFunction: tileUrlFunction,
transition: options.transition
});
/**
* @inheritDoc
*/
this.zDirection = options.zDirection;
}
}
export default IIIF;

View File

@@ -237,6 +237,7 @@ class TileGrid {
/**
* Get the extent for this tile grid, if it was configured.
* @return {import("../extent.js").Extent} Extent.
* @api
*/
getExtent() {
return this.extent_;

View File

@@ -0,0 +1,487 @@
import IIIFInfo from '../../../../src/ol/format/IIIFInfo.js';
import {Versions} from '../../../../src/ol/format/IIIFInfo.js';
describe('ol.format.IIIFInfo', function() {
const iiifInfo = new IIIFInfo();
describe('setImageInfo', function() {
it('can handle image info JSON as object or as string serialization', function() {
iiifInfo.setImageInfo({
'@context': 'http://iiif.io/api/image/3/context.json',
'@id': 'http://iiif.test/id'
});
expect(iiifInfo.getImageApiVersion()).to.be(Versions.VERSION3);
iiifInfo.setImageInfo('{"@context": "http://iiif.io/api/image/2/context.json","@id":"http://iiif.test/id"}');
expect(iiifInfo.getImageApiVersion()).to.be(Versions.VERSION2);
});
});
describe('getImageApiVersion', function() {
it('provides the correct Image API version', function() {
iiifInfo.setImageInfo({
'@id': 'http://iiif.test/id'
});
expect(function() {
iiifInfo.getImageApiVersion();
}).to.throwException();
iiifInfo.setImageInfo({
identifier: 'http://iiif.test/id',
profile: 'this is no valid profile'
});
expect(function() {
iiifInfo.getImageApiVersion();
}).to.throwException();
iiifInfo.setImageInfo({
'@context': 'this is no valid context',
'@id': 'http://iiif.test/id'
});
expect(function() {
iiifInfo.getImageApiVersion();
}).to.throwException();
iiifInfo.setImageInfo({
identifier: 'http://iiif.test/id',
profile: 'http://library.stanford.edu/iiif/image-api/compliance.html#level0'
});
expect(iiifInfo.getImageApiVersion()).to.be(Versions.VERSION1);
iiifInfo.setImageInfo({
'@context': 'http://library.stanford.edu/iiif/image-api/1.1/context.json',
'@id': 'http://iiif.test/id'
});
expect(iiifInfo.getImageApiVersion()).to.be(Versions.VERSION1);
iiifInfo.setImageInfo({
'@context': 'http://iiif.io/api/image/1/context.json',
identifier: 'http://iiif.test/id'
});
expect(iiifInfo.getImageApiVersion()).to.be(Versions.VERSION1);
iiifInfo.setImageInfo({
'@context': 'http://iiif.io/api/image/2/context.json',
'@id': 'http://iiif.test/id'
});
expect(iiifInfo.getImageApiVersion()).to.be(Versions.VERSION2);
iiifInfo.setImageInfo({
'@context': 'http://iiif.io/api/image/3/context.json',
id: 'http://iiif.test/id'
});
expect(iiifInfo.getImageApiVersion()).to.be(Versions.VERSION3);
});
});
describe('getComplianceLevelFromProfile', function() {
it('detects the correct compliance level', function() {
iiifInfo.setImageInfo({
'@context': 'http://iiif.io/api/image/2/context.json',
profile: 'level0'
});
expect(iiifInfo.getComplianceLevelFromProfile()).to.be(undefined);
iiifInfo.setImageInfo({
'@context': 'http://iiif.io/api/image/2/context.json',
profile: 'http://iiif.io/api/image/level3.json'
});
expect(iiifInfo.getComplianceLevelFromProfile()).to.be(undefined);
iiifInfo.setImageInfo({
'@context': 'http://iiif.io/api/image/2/context.json',
profile: 'level1'
});
expect(iiifInfo.getComplianceLevelFromProfile()).to.be(undefined);
iiifInfo.setImageInfo({
'@context': 'http://iiif.io/api/image/2/context.json',
profile: 'http://iiif.io/api/image/2/level2.json'
});
expect(iiifInfo.getComplianceLevelFromProfile()).to.be('level2');
iiifInfo.setImageInfo({
'@context': 'http://iiif.io/api/image/2/context.json',
profile: ['http://iiif.io/api/image/2/level1.json']
});
expect(iiifInfo.getComplianceLevelFromProfile()).to.be('level1');
iiifInfo.setImageInfo({
'@context': 'http://iiif.io/api/image/3/context.json',
profile: 'level4'
});
expect(iiifInfo.getComplianceLevelFromProfile()).to.be(undefined);
iiifInfo.setImageInfo({
'@context': 'http://iiif.io/api/image/3/context.json',
profile: 'http://iiif.io/api/image/3/level3.json'
});
expect(iiifInfo.getComplianceLevelFromProfile()).to.be(undefined);
iiifInfo.setImageInfo({
'@context': 'http://iiif.io/api/image/3/context.json',
profile: 'http://iiif.io/api/image/2/level1.json'
});
expect(iiifInfo.getComplianceLevelFromProfile()).to.be(undefined);
iiifInfo.setImageInfo({
'@context': 'http://iiif.io/api/image/3/context.json',
profile: 'level2'
});
expect(iiifInfo.getComplianceLevelFromProfile()).to.be('level2');
iiifInfo.setImageInfo({
'@context': 'http://iiif.io/api/image/3/context.json',
profile: 'http://iiif.io/api/image/3/level1.json'
});
expect(iiifInfo.getComplianceLevelFromProfile()).to.be('level1');
});
});
describe('getComplianceLevelSupportedFeatures', function() {
it('provides the correct features for given versions and compliance levels', function() {
iiifInfo.setImageInfo({
'@context': 'http://library.stanford.edu/iiif/image-api/1.1/context.json',
profile: 'http://library.stanford.edu/iiif/image-api/compliance.html#level0'
});
let level = iiifInfo.getComplianceLevelSupportedFeatures();
expect(level.supports).to.be.empty();
iiifInfo.setImageInfo({
'@context': 'http://library.stanford.edu/iiif/image-api/1.1/context.json',
profile: 'http://library.stanford.edu/iiif/image-api/compliance.html#level1'
});
level = iiifInfo.getComplianceLevelSupportedFeatures();
expect(level.supports).to.have.length(4);
expect(level.supports).to.contain('regionByPx');
expect(level.supports).to.contain('sizeByW');
expect(level.supports).to.contain('sizeByH');
expect(level.supports).to.contain('sizeByPct');
iiifInfo.setImageInfo({
'@context': 'http://library.stanford.edu/iiif/image-api/1.1/context.json',
profile: 'http://library.stanford.edu/iiif/image-api/compliance.html#level2'
});
level = iiifInfo.getComplianceLevelSupportedFeatures();
expect(level.supports).to.have.length(7);
expect(level.supports).to.contain('regionByPx');
expect(level.supports).to.contain('regionByPct');
expect(level.supports).to.contain('sizeByW');
expect(level.supports).to.contain('sizeByH');
expect(level.supports).to.contain('sizeByPct');
expect(level.supports).to.contain('sizeByConfinedWh');
expect(level.supports).to.contain('sizeByWh');
iiifInfo.setImageInfo({
'@context': 'http://iiif.io/api/image/2/context.json',
profile: 'http://iiif.io/api/image/2/level0.json'
});
level = iiifInfo.getComplianceLevelSupportedFeatures();
expect(level.supports).to.be.empty();
iiifInfo.setImageInfo({
'@context': 'http://iiif.io/api/image/2/context.json',
profile: 'http://iiif.io/api/image/2/level1.json'
});
level = iiifInfo.getComplianceLevelSupportedFeatures();
expect(level.supports).to.have.length(4);
expect(level.supports).to.contain('regionByPx');
expect(level.supports).to.contain('sizeByW');
expect(level.supports).to.contain('sizeByH');
expect(level.supports).to.contain('sizeByPct');
iiifInfo.setImageInfo({
'@context': 'http://iiif.io/api/image/2/context.json',
profile: 'http://iiif.io/api/image/2/level2.json'
});
level = iiifInfo.getComplianceLevelSupportedFeatures();
expect(level.supports).to.have.length(8);
expect(level.supports).to.contain('regionByPx');
expect(level.supports).to.contain('regionByPct');
expect(level.supports).to.contain('sizeByW');
expect(level.supports).to.contain('sizeByH');
expect(level.supports).to.contain('sizeByPct');
expect(level.supports).to.contain('sizeByConfinedWh');
expect(level.supports).to.contain('sizeByDistortedWh');
expect(level.supports).to.contain('sizeByWh');
// TODO test version 3 compliance level features once version 3 is final
});
});
describe('getTileSourceOptions', function() {
it('produces options from minimal information responses', function() {
expect(function() {
iiifInfo.setImageInfo({
width: 2000,
height: 1500
});
iiifInfo.getTileSourceOptions();
}).to.throwException();
iiifInfo.setImageInfo({
identifier: 'id',
profile: 'http://library.stanford.edu/iiif/image-api/compliance.html#level0'
});
let options = iiifInfo.getTileSourceOptions();
expect(options).to.be.an('object');
expect(options).to.have.property('version', Versions.VERSION1);
iiifInfo.setImageInfo({
identifier: 'identifier-version-1.0',
width: 2000,
height: 1500,
profile: 'http://library.stanford.edu/iiif/image-api/compliance.html#level0'
});
options = iiifInfo.getTileSourceOptions();
expect(options).to.not.be(undefined);
expect(options).to.not.be(null);
expect(options).to.have.property('version', Versions.VERSION1);
expect(options).to.have.property('size');
expect(options.size).to.be.an('array');
expect(options.size.length).to.be(2);
expect(options.size[0]).to.be(2000);
expect(options.size[1]).to.be(1500);
expect(options.quality).to.be('native');
expect(options.url).to.be(undefined);
expect(options.sizes).to.be(undefined);
expect(options.tileSize).to.be(undefined);
expect(options.format).to.be('jpg');
expect(options.supports).to.be.empty();
iiifInfo.setImageInfo({
'@context': 'http://iiif.io/api/image/2/context.json',
'@id': 'http://iiif.test/version2/id'
});
options = iiifInfo.getTileSourceOptions();
expect(options).to.be.an('object');
expect(options).to.have.property('version', Versions.VERSION2);
expect(options).to.have.property('url', 'http://iiif.test/version2/id');
expect(options).to.have.property('format', 'jpg');
});
it('uses preferred options if applicable', function() {
iiifInfo.setImageInfo({
'@context': 'http://iiif.io/api/image/2/context.json',
'@id': 'http://iiif.test/version2/id',
width: 2000,
height: 1500,
profile: ['http://iiif.io/api/image/2/level2.json']
});
const options = iiifInfo.getTileSourceOptions({
quality: 'bitonal',
format: 'png'
});
expect(options).to.have.property('quality', 'bitonal');
expect(options).to.have.property('format', 'png');
});
it('ignores preferred options that are not supported', function() {
iiifInfo.setImageInfo({
'@context': 'http://iiif.io/api/image/2/context.json',
'@id': 'http://iiif.test/version2/id',
width: 2000,
height: 1500,
profile: ['http://iiif.io/api/image/2/level1.json']
});
const options = iiifInfo.getTileSourceOptions({
quality: 'bitonal',
format: 'png'
});
expect(options).to.have.property('quality', 'default');
expect(options).to.have.property('format', 'jpg');
});
it('combines supported features indicated by compliance level and explicitly stated in image info', function() {
iiifInfo.setImageInfo({
'@context': 'http://iiif.io/api/image/2/context.json',
'@id': 'http://iiif.test/id',
profile: ['http://iiif.io/api/image/2/level1.json', {
supports: ['regionByPct', 'sizeByWh']
}]
});
let options = iiifInfo.getTileSourceOptions();
expect(options.supports).to.contain('regionByPct');
expect(options.supports).to.contain('sizeByWh');
expect(options.supports).to.contain('regionByPx');
expect(options.supports).to.contain('sizeByW');
expect(options.supports).to.contain('sizeByH');
expect(options.supports).to.contain('sizeByPct');
expect(options.supports).to.have.length(6);
iiifInfo.setImageInfo({
'@context': 'http://iiif.io/api/image/3/context.json',
id: 'http://iiif.test/id',
profile: 'level1',
extraFeatures: ['regionByPct', 'sizeByPct']
});
options = iiifInfo.getTileSourceOptions();
expect(options.supports).to.contain('regionByPct');
expect(options.supports).to.contain('sizeByPct');
expect(options.supports).to.contain('regionByPx');
expect(options.supports).to.contain('regionSquare');
expect(options.supports).to.contain('sizeByW');
expect(options.supports).to.contain('sizeByH');
expect(options.supports).to.have.length(6);
});
it('uses the first available scale factors and tile sizes', function() {
iiifInfo.setImageInfo({
'@context': 'http://library.stanford.edu/iiif/image-api/1.1/context.json',
'@id': 'http://iiif.test/id',
profile: 'http://library.stanford.edu/iiif/image-api/compliance.html#level0'
});
let options = iiifInfo.getTileSourceOptions();
expect(options.resolutions).to.be(undefined);
expect(options.tileSize).to.be(undefined);
iiifInfo.setImageInfo({
'@context': 'http://library.stanford.edu/iiif/image-api/1.1/context.json',
'@id': 'http://iiif.test/id',
profile: 'http://library.stanford.edu/iiif/image-api/compliance.html#level0',
scale_factors: [1, 2, 4],
tile_width: 512
});
options = iiifInfo.getTileSourceOptions();
expect(options.resolutions).to.have.length(3);
expect(options.resolutions).to.contain(1);
expect(options.resolutions).to.contain(2);
expect(options.resolutions).to.contain(4);
expect(options.tileSize).to.have.length(2);
expect(options.tileSize[0]).to.be(512);
expect(options.tileSize[1]).to.be(512);
iiifInfo.setImageInfo({
'@context': 'http://library.stanford.edu/iiif/image-api/1.1/context.json',
'@id': 'http://iiif.test/id',
profile: 'http://library.stanford.edu/iiif/image-api/compliance.html#level0',
scale_factors: [1, 2, 4],
tile_width: 512,
tile_height: 1024
});
options = iiifInfo.getTileSourceOptions();
expect(options.resolutions).to.have.length(3);
expect(options.resolutions).to.contain(1);
expect(options.resolutions).to.contain(2);
expect(options.resolutions).to.contain(4);
expect(options.tileSize).to.have.length(2);
expect(options.tileSize[0]).to.be(512);
expect(options.tileSize[1]).to.be(1024);
iiifInfo.setImageInfo({
'@context': 'http://iiif.io/api/image/2/context.json',
'@id': 'http://iiif.test/id',
profile: 'http://iiif.io/api/image/2/level0.json'
});
options = iiifInfo.getTileSourceOptions();
expect(options.resolutions).to.be(undefined);
expect(options.tileSize).to.be(undefined);
iiifInfo.setImageInfo({
'@context': 'http://iiif.io/api/image/2/context.json',
'@id': 'http://iiif.test/id',
profile: 'http://iiif.io/api/image/2/level0.json',
tiles: [{
scaleFactors: [1, 2, 4],
width: 512
},
{
scaleFactors: [1, 2, 4, 8, 16],
width: 256
}]
});
options = iiifInfo.getTileSourceOptions();
expect(options.resolutions).to.have.length(3);
expect(options.resolutions).to.contain(1);
expect(options.resolutions).to.contain(2);
expect(options.resolutions).to.contain(4);
expect(options.tileSize).to.have.length(2);
expect(options.tileSize[0]).to.be(512);
expect(options.tileSize[1]).to.be(512);
iiifInfo.setImageInfo({
'@context': 'http://iiif.io/api/image/2/context.json',
'@id': 'http://iiif.test/id',
profile: 'http://iiif.io/api/image/2/level0.json',
tiles: [{
scaleFactors: [1, 2, 4],
width: 512,
height: 1024
}]
});
options = iiifInfo.getTileSourceOptions();
expect(options.resolutions).to.have.length(3);
expect(options.resolutions).to.contain(1);
expect(options.resolutions).to.contain(2);
expect(options.resolutions).to.contain(4);
expect(options.tileSize).to.have.length(2);
expect(options.tileSize[0]).to.be(512);
expect(options.tileSize[1]).to.be(1024);
});
});
it('provides each given size in sizes as OpenLayers Size', function() {
iiifInfo.setImageInfo({
'@context': 'http://iiif.io/api/image/2/context.json',
'@id': 'http://iiif.test/id',
'sizes': [{
width: 2000,
height: 1000
},
{
width: 1000,
height: 500
},
{
width: 500,
height: 250
}]
});
const options = iiifInfo.getTileSourceOptions();
expect(options.sizes).to.have.length(3);
expect(options.sizes[0]).to.have.length(2);
expect(options.sizes[0][0]).to.be(2000);
expect(options.sizes[0][1]).to.be(1000);
expect(options.sizes[1]).to.have.length(2);
expect(options.sizes[1][0]).to.be(1000);
expect(options.sizes[1][1]).to.be(500);
expect(options.sizes[2]).to.have.length(2);
expect(options.sizes[2][0]).to.be(500);
expect(options.sizes[2][1]).to.be(250);
});
});

View File

@@ -0,0 +1,387 @@
import {DEFAULT_TILE_SIZE} from '../../../../src/ol/tilegrid/common.js';
import IIIF from '../../../../src/ol/source/IIIF.js';
import {Versions} from '../../../../src/ol/format/IIIFInfo.js';
describe('ol.source.IIIF', function() {
const width = 2000,
height = 1500,
size = [width, height],
url = 'http://iiif.test/image-id';
function getMinimalSource() {
return new IIIF({
size: size
});
}
function getSource(additionalOptions) {
const options = Object.assign({}, {
size: size,
url: url
}, additionalOptions === undefined ? {} : additionalOptions);
return new IIIF(options);
}
describe('constructor', function() {
it('requires valid size option', function() {
expect(function() {
new IIIF();
}).to.throwException();
expect(function() {
new IIIF({});
}).to.throwException();
expect(function() {
new IIIF({
size: []
});
}).to.throwException();
expect(function() {
new IIIF({
size: 100
});
}).to.throwException();
expect(function() {
new IIIF({
size: [100]
});
}).to.throwException();
expect(function() {
new IIIF({
size: [null, 100]
});
}).to.throwException();
expect(function() {
new IIIF({
size: ['very wide', 100]
});
}).to.throwException();
expect(function() {
new IIIF({
size: [0, 100]
});
}).to.throwException();
expect(function() {
new IIIF({
size: [100, null]
});
}).to.throwException();
expect(function() {
new IIIF({
size: [100, 0]
});
}).to.throwException();
expect(function() {
new IIIF({
size: [100, 'not that high']
});
}).to.throwException();
expect(function() {
new IIIF({
size: [100, 200, 300]
});
}).to.throwException();
let source;
expect(function() {
source = new IIIF({
size: [100, 200]
});
}).to.not.throwException();
expect(source).to.be.a(IIIF);
expect(function() {
getMinimalSource();
}).to.not.throwException();
});
it('uses empty base URL, default quality, jpg format as default', function() {
const tileUrlFunction = getMinimalSource().getTileUrlFunction();
expect(tileUrlFunction([0, 0, 0])).to.be('full/full/0/default.jpg');
});
it('uses native as default quality for version 1', function() {
const tileUrlFunction = new IIIF({
size: size,
version: Versions.VERSION1
}).getTileUrlFunction();
expect(tileUrlFunction([0, 0, 0])).to.be('full/full/0/native.jpg');
});
it('corrects non empty base URL if trailing slash is missing', function() {
// missing trailing slash is added
let tileUrlFunction = getSource().getTileUrlFunction();
expect(tileUrlFunction([0, 0, 0])).to.be('http://iiif.test/image-id/full/full/0/default.jpg');
// existent trailing slash isn't doubled
tileUrlFunction = getSource({
url: 'http://iiif.test/other-image-id/'
}).getTileUrlFunction();
expect(tileUrlFunction([0, 0, 0])).to.be('http://iiif.test/other-image-id/full/full/0/default.jpg');
});
});
describe('tileUrlFunction', function() {
it('has only one resolution and one tile if no tiles, resolutions, sizes and supported features are given', function() {
let tileUrlFunction = getSource().getTileUrlFunction();
expect(tileUrlFunction([0, 0, 0])).to.be('http://iiif.test/image-id/full/full/0/default.jpg');
expect(tileUrlFunction([-1, 0, 0])).to.be(undefined);
expect(tileUrlFunction([1, 0, 0])).to.be(undefined);
expect(tileUrlFunction([0, 1, 0])).to.be(undefined);
expect(tileUrlFunction([0, 0, 1])).to.be(undefined);
tileUrlFunction = getSource({
version: Versions.VERSION1
}).getTileUrlFunction();
expect(tileUrlFunction([0, 0, 0])).to.be('http://iiif.test/image-id/full/full/0/native.jpg');
tileUrlFunction = getSource({
version: Versions.VERSION3
}).getTileUrlFunction();
expect(tileUrlFunction([0, 0, 0])).to.be('http://iiif.test/image-id/full/max/0/default.jpg');
});
it('constructs the same number of resolutions as distinguishable sizes are given', function() {
let tileUrlFunction = getSource({
sizes: [[2000, 1500], [1000, 750], [500, 375]]
}).getTileUrlFunction();
expect(tileUrlFunction([0, 0, 0])).to.be('http://iiif.test/image-id/full/500,/0/default.jpg');
expect(tileUrlFunction([1, 0, 0])).to.be('http://iiif.test/image-id/full/1000,/0/default.jpg');
expect(tileUrlFunction([2, 0, 0])).to.be('http://iiif.test/image-id/full/full/0/default.jpg');
expect(tileUrlFunction([3, 0, 0])).to.be(undefined);
expect(tileUrlFunction([-1, 0, 0])).to.be(undefined);
expect(tileUrlFunction([0, 1, 0])).to.be(undefined);
expect(tileUrlFunction([0, 0, 1])).to.be(undefined);
expect(tileUrlFunction([1, 1, 0])).to.be(undefined);
expect(tileUrlFunction([1, 0, 1])).to.be(undefined);
tileUrlFunction = getSource({
sizes: [[2000, 1500], [1000, 750], [500, 375]],
version: Versions.VERSION3
}).getTileUrlFunction();
expect(tileUrlFunction([0, 0, 0])).to.be('http://iiif.test/image-id/full/500,375/0/default.jpg');
expect(tileUrlFunction([1, 0, 0])).to.be('http://iiif.test/image-id/full/1000,750/0/default.jpg');
expect(tileUrlFunction([2, 0, 0])).to.be('http://iiif.test/image-id/full/max/0/default.jpg');
tileUrlFunction = getSource({
sizes: [[2000, 1500], [1000, 749], [1000, 750], [500, 375], [500, 374]]
}).getTileUrlFunction();
expect(tileUrlFunction([0, 0, 0])).to.be('http://iiif.test/image-id/full/500,/0/default.jpg');
expect(tileUrlFunction([1, 0, 0])).to.be('http://iiif.test/image-id/full/1000,/0/default.jpg');
expect(tileUrlFunction([2, 0, 0])).to.be('http://iiif.test/image-id/full/full/0/default.jpg');
expect(tileUrlFunction([3, 0, 0])).to.be(undefined);
});
it('cannot provide scaled tiles without provided tilesize or supported features', function() {
const tileUrlFunction = getSource({
resolutions: [16, 8, 4, 2, 1]
}).getTileUrlFunction();
expect(tileUrlFunction([0, 0, 0])).to.be('http://iiif.test/image-id/full/full/0/default.jpg');
expect(tileUrlFunction([-1, 0, 0])).to.be(undefined);
expect(tileUrlFunction([1, 0, 0])).to.be(undefined);
expect(tileUrlFunction([0, 1, 0])).to.be(undefined);
expect(tileUrlFunction([0, 0, 1])).to.be(undefined);
});
it('provides canonical tile URLs for all necessary resolutions if only a tileSize exists', function() {
let tileUrlFunction = getSource({
tileSize: 512
}).getTileUrlFunction();
expect(tileUrlFunction([0, 0, 0])).to.be('http://iiif.test/image-id/full/500,/0/default.jpg');
expect(tileUrlFunction([-1, 0, 0])).to.be(undefined);
expect(tileUrlFunction([0, 1, 0])).to.be(undefined);
expect(tileUrlFunction([0, 0, 1])).to.be(undefined);
expect(tileUrlFunction([1, 0, 0])).to.be('http://iiif.test/image-id/0,0,1024,1024/512,/0/default.jpg');
expect(tileUrlFunction([1, 1, 0])).to.be('http://iiif.test/image-id/1024,0,976,1024/488,/0/default.jpg');
expect(tileUrlFunction([1, 0, 1])).to.be('http://iiif.test/image-id/0,1024,1024,476/512,/0/default.jpg');
expect(tileUrlFunction([1, 1, 1])).to.be('http://iiif.test/image-id/1024,1024,976,476/488,/0/default.jpg');
expect(tileUrlFunction([2, 0, 0])).to.be('http://iiif.test/image-id/0,0,512,512/512,/0/default.jpg');
expect(tileUrlFunction([2, 3, 0])).to.be('http://iiif.test/image-id/1536,0,464,512/464,/0/default.jpg');
expect(tileUrlFunction([2, 0, 2])).to.be('http://iiif.test/image-id/0,1024,512,476/512,/0/default.jpg');
expect(tileUrlFunction([2, 3, 2])).to.be('http://iiif.test/image-id/1536,1024,464,476/464,/0/default.jpg');
expect(tileUrlFunction([3, 0, 0])).to.be(undefined);
tileUrlFunction = getSource({
tileSize: 512,
version: Versions.VERSION3
}).getTileUrlFunction();
expect(tileUrlFunction([0, 0, 0])).to.be('http://iiif.test/image-id/full/500,375/0/default.jpg');
expect(tileUrlFunction([1, 0, 0])).to.be('http://iiif.test/image-id/0,0,1024,1024/512,512/0/default.jpg');
expect(tileUrlFunction([1, 1, 0])).to.be('http://iiif.test/image-id/1024,0,976,1024/488,512/0/default.jpg');
expect(tileUrlFunction([1, 0, 1])).to.be('http://iiif.test/image-id/0,1024,1024,476/512,238/0/default.jpg');
expect(tileUrlFunction([1, 1, 1])).to.be('http://iiif.test/image-id/1024,1024,976,476/488,238/0/default.jpg');
expect(tileUrlFunction([2, 0, 0])).to.be('http://iiif.test/image-id/0,0,512,512/512,512/0/default.jpg');
expect(tileUrlFunction([2, 3, 0])).to.be('http://iiif.test/image-id/1536,0,464,512/464,512/0/default.jpg');
expect(tileUrlFunction([2, 0, 2])).to.be('http://iiif.test/image-id/0,1024,512,476/512,476/0/default.jpg');
expect(tileUrlFunction([2, 3, 2])).to.be('http://iiif.test/image-id/1536,1024,464,476/464,476/0/default.jpg');
});
it('provides canonical tile URLs for all provided resolutions if a tileSize also exists', function() {
const tileUrlFunction = getSource({
tileSize: 512,
resolutions: [8, 4, 2, 1]
}).getTileUrlFunction();
expect(tileUrlFunction([0, 0, 0])).to.be('http://iiif.test/image-id/full/250,/0/default.jpg');
expect(tileUrlFunction([1, 0, 0])).to.be('http://iiif.test/image-id/full/500,/0/default.jpg');
expect(tileUrlFunction([2, 0, 0])).to.be('http://iiif.test/image-id/0,0,1024,1024/512,/0/default.jpg');
expect(tileUrlFunction([2, 1, 0])).to.be('http://iiif.test/image-id/1024,0,976,1024/488,/0/default.jpg');
expect(tileUrlFunction([2, 0, 1])).to.be('http://iiif.test/image-id/0,1024,1024,476/512,/0/default.jpg');
expect(tileUrlFunction([2, 1, 1])).to.be('http://iiif.test/image-id/1024,1024,976,476/488,/0/default.jpg');
expect(tileUrlFunction([3, 0, 0])).to.be('http://iiif.test/image-id/0,0,512,512/512,/0/default.jpg');
expect(tileUrlFunction([3, 3, 0])).to.be('http://iiif.test/image-id/1536,0,464,512/464,/0/default.jpg');
expect(tileUrlFunction([3, 0, 2])).to.be('http://iiif.test/image-id/0,1024,512,476/512,/0/default.jpg');
expect(tileUrlFunction([3, 3, 2])).to.be('http://iiif.test/image-id/1536,1024,464,476/464,/0/default.jpg');
expect(tileUrlFunction([4, 0, 0])).to.be(undefined);
});
it('supports non square tiles', function() {
let tileUrlFunction = getSource({
tileSize: [1024, 512]
}).getTileUrlFunction();
expect(tileUrlFunction([0, 0, 0])).to.be('http://iiif.test/image-id/full/500,/0/default.jpg');
expect(tileUrlFunction([1, 0, 0])).to.be('http://iiif.test/image-id/0,0,2000,1024/1000,/0/default.jpg');
expect(tileUrlFunction([1, 0, 1])).to.be('http://iiif.test/image-id/0,1024,2000,476/1000,/0/default.jpg');
expect(tileUrlFunction([2, 0, 0])).to.be('http://iiif.test/image-id/0,0,1024,512/1024,/0/default.jpg');
expect(tileUrlFunction([2, 1, 0])).to.be('http://iiif.test/image-id/1024,0,976,512/976,/0/default.jpg');
expect(tileUrlFunction([2, 0, 2])).to.be('http://iiif.test/image-id/0,1024,1024,476/1024,/0/default.jpg');
expect(tileUrlFunction([2, 1, 2])).to.be('http://iiif.test/image-id/1024,1024,976,476/976,/0/default.jpg');
expect(tileUrlFunction([3, 0, 0])).to.be(undefined);
tileUrlFunction = getSource({
tileSize: [1024, 512],
version: Versions.VERSION3
}).getTileUrlFunction();
expect(tileUrlFunction([0, 0, 0])).to.be('http://iiif.test/image-id/full/500,375/0/default.jpg');
expect(tileUrlFunction([2, 0, 0])).to.be('http://iiif.test/image-id/0,0,1024,512/1024,512/0/default.jpg');
});
it('provides tile URLs with default tile size if sufficient supported features are provided', function() {
let tileUrlFunction = getSource({
supports: ['regionByPx', 'sizeByW']
}).getTileUrlFunction();
const maxZoom = Math.ceil(Math.log2(width / DEFAULT_TILE_SIZE));
expect(tileUrlFunction([maxZoom, 0, 0])).to.be('http://iiif.test/image-id/0,0,' + DEFAULT_TILE_SIZE + ',' + DEFAULT_TILE_SIZE + '/' + DEFAULT_TILE_SIZE + ',/0/default.jpg');
expect(tileUrlFunction([maxZoom + 1, 0, 0])).to.be(undefined);
tileUrlFunction = getSource({
supports: ['regionByPx', 'sizeByH']
}).getTileUrlFunction();
expect(tileUrlFunction([maxZoom, 0, 0])).to.be('http://iiif.test/image-id/0,0,' + DEFAULT_TILE_SIZE + ',' + DEFAULT_TILE_SIZE + '/,' + DEFAULT_TILE_SIZE + '/0/default.jpg');
expect(tileUrlFunction([maxZoom + 1, 0, 0])).to.be(undefined);
tileUrlFunction = getSource({
supports: ['regionByPx', 'sizeByWh']
}).getTileUrlFunction();
expect(tileUrlFunction([maxZoom, 0, 0])).to.be('http://iiif.test/image-id/0,0,' + DEFAULT_TILE_SIZE + ',' + DEFAULT_TILE_SIZE + '/' + DEFAULT_TILE_SIZE + ',' + DEFAULT_TILE_SIZE + '/0/default.jpg');
expect(tileUrlFunction([maxZoom + 1, 0, 0])).to.be(undefined);
tileUrlFunction = getSource({
supports: ['regionByPct', 'sizeByPct']
}).getTileUrlFunction();
const tileWPct = (DEFAULT_TILE_SIZE / width * 100).toLocaleString('en', {maximumFractionDigits: 10}),
tileHPct = (DEFAULT_TILE_SIZE / height * 100).toLocaleString('en', {maximumFractionDigits: 10});
expect(tileUrlFunction([maxZoom, 0, 0])).to.be('http://iiif.test/image-id/pct:0,0,' + tileWPct + ',' + tileHPct + '/pct:100/0/default.jpg');
expect(tileUrlFunction([maxZoom + 1, 0, 0])).to.be(undefined);
});
it('prefers canonical tile URLs', function() {
let tileUrlFunction = getSource({
tileSize: 512,
supports: ['regionByPx', 'regionByPct', 'sizeByW', 'sizeByH', 'sizeByWh', 'sizeByPct']
}).getTileUrlFunction();
expect(tileUrlFunction([2, 0, 0])).to.be('http://iiif.test/image-id/0,0,512,512/512,/0/default.jpg');
tileUrlFunction = getSource({
tileSize: 512,
version: Versions.VERSION3,
supports: ['regionByPx', 'regionByPct', 'sizeByW', 'sizeByH', 'sizeByWh', 'sizeByPct']
}).getTileUrlFunction();
expect(tileUrlFunction([2, 0, 0])).to.be('http://iiif.test/image-id/0,0,512,512/512,512/0/default.jpg');
});
it('provides correct tile URLs for percentage URL parameter values', function() {
const tileUrlFunction = getSource({
tileSize: 512,
supports: ['regionByPct', 'sizeByPct']
}).getTileUrlFunction();
expect(tileUrlFunction([0, 0, 0])).to.be('http://iiif.test/image-id/full/pct:25/0/default.jpg');
expect(tileUrlFunction([-1, 0, 0])).to.be(undefined);
expect(tileUrlFunction([0, 1, 0])).to.be(undefined);
expect(tileUrlFunction([0, 0, 1])).to.be(undefined);
expect(tileUrlFunction([1, 0, 0])).to.be('http://iiif.test/image-id/pct:0,0,51.2,68.2666666667/pct:50/0/default.jpg');
expect(tileUrlFunction([1, 1, 0])).to.be('http://iiif.test/image-id/pct:51.2,0,48.8,68.2666666667/pct:50/0/default.jpg');
expect(tileUrlFunction([1, 0, 1])).to.be('http://iiif.test/image-id/pct:0,68.2666666667,51.2,31.7333333333/pct:50/0/default.jpg');
expect(tileUrlFunction([1, 1, 1])).to.be('http://iiif.test/image-id/pct:51.2,68.2666666667,48.8,31.7333333333/pct:50/0/default.jpg');
expect(tileUrlFunction([2, 0, 0])).to.be('http://iiif.test/image-id/pct:0,0,25.6,34.1333333333/pct:100/0/default.jpg');
expect(tileUrlFunction([2, 3, 0])).to.be('http://iiif.test/image-id/pct:76.8,0,23.2,34.1333333333/pct:100/0/default.jpg');
expect(tileUrlFunction([2, 0, 2])).to.be('http://iiif.test/image-id/pct:0,68.2666666667,25.6,31.7333333333/pct:100/0/default.jpg');
expect(tileUrlFunction([2, 3, 2])).to.be('http://iiif.test/image-id/pct:76.8,68.2666666667,23.2,31.7333333333/pct:100/0/default.jpg');
expect(tileUrlFunction([3, 0, 0])).to.be(undefined);
});
});
});