mirador
Version:
An open-source, web-based 'multi-up' viewer that supports zoom-pan-rotate functionality, ability to display/compare simple images, and images with annotations.
481 lines (440 loc) • 13.2 kB
JavaScript
import { createSelector } from 'reselect';
import { PropertyValue, Utils, Resource } from 'manifesto.js';
import asArray from '../../lib/asArray';
import { getCompanionWindowLocale } from './companionWindows';
import { getManifest } from './getters';
import { getConfig } from './config';
import { getThumbnailFactory } from './thumbnails';
/** */
function createManifestoInstance(json, locale) {
if (!json) return undefined;
// Use structuredClone to create a deep copy and prevent Manifesto from mutating the json
const manifestoObject = Utils.parseManifest(structuredClone(json), locale ? { locale } : undefined);
if (manifestoObject) {
// Local patching of Manifesto so that when its a Collection, it behaves similarly
if (typeof manifestoObject.getSequences != 'function') {
manifestoObject.getSequences = () => [];
}
return manifestoObject;
}
return undefined;
}
/** */
export const getLocale = createSelector(
[
getCompanionWindowLocale,
getConfig,
(state, { locale }) => locale,
],
(companionWindowLocale, config = {}, locale) => (
locale || companionWindowLocale || config.language || config.fallbackLanguages
),
);
const defaultManifestStatus = Object.freeze({ missing: true });
/**
* Convenience selector to get a manifest (or placeholder).
* @param {object} state
* @param {object} props
* @param {string} props.windowId
* @returns {object} {error: null: id: string, isFetching: boolean, json: {...}}
*/
export const getManifestStatus = createSelector(
[getManifest],
manifest => manifest || defaultManifestStatus,
);
/**
* Convenience selector to get a manifest loading error
* @param {object} state
* @param {object} props
* @param {string} props.manifestId
* @returns {string|null}
*/
export const getManifestError = createSelector(
[getManifest],
manifest => manifest && manifest.error,
);
/** Instantiate a manifesto instance */
const getContextualManifestoInstance = createSelector(
getManifest,
manifest => manifest && createManifestoInstance(manifest.json),
);
/**
* Instantiate a manifesto instance
* @param {object} state
* @param {object} props
* @param {string} props.manifestId
* @returns {object}
*/
export const getManifestoInstance = createSelector(
getContextualManifestoInstance,
(state, { json }) => json,
(manifesto, manifestJson) => (
manifestJson && createManifestoInstance(manifestJson, locale)
) || manifesto,
);
export const getManifestLocale = createSelector(
[getManifestoInstance, getLocale],
(manifest, locale) => locale ?? (manifest && manifest.options && manifest.options.locale && manifest.options.locale.replace(/-.*$/, '')),
);
/** */
function getProperty(property) {
return createSelector(
[getManifestoInstance],
manifest => manifest && manifest.getProperty(property),
);
}
/**
* Return the IIIF v3 provider of a manifest or null.
* @param {object} state
* @param {object} props
* @param {string} props.manifestId
* @param {string} props.windowId
* @returns {string|null}
*/
export const getManifestProviderName = createSelector(
[
getProperty('provider'),
getManifestLocale,
],
(provider, locale) => provider
&& provider[0].label
&& PropertyValue.parse(provider[0].label).getValue(locale),
);
const EMPTY_LOGO_OPTS = Object.freeze({});
/**
* Return the IIIF v3 provider logo.
* @param {object} state
* @param {object} props
* @returns {string|null}
*/
export const getProviderLogo = createSelector(
[
getProperty('provider'),
(state) => getThumbnailFactory(state, EMPTY_LOGO_OPTS),
],
(provider, thumbnailFactory) => {
const logo = provider && provider[0] && provider[0].logo && provider[0].logo[0];
if (!logo) return null;
return thumbnailFactory.get(new Resource(logo))?.url;
},
);
/**
* Get the logo for a manifest.
* @param {object} state
* @param {object} props
* @returns {string|null}
*/
export const getManifestLogo = createSelector(
[getManifestoInstance, getProviderLogo],
(manifest, v3logo) => v3logo || (manifest && manifest.getLogo()),
);
/**
* Return the IIIF v3 homepage of a manifest or null.
* @param {object} state
* @param {object} props
* @param {string} props.manifestId
* @param {string} props.windowId
* @returns {string|null}
*/
export const getManifestHomepage = createSelector(
[
getProperty('homepage'),
getManifestLocale,
],
(homepages, locale) => homepages
&& asArray(homepages).map(homepage => (
{
label: PropertyValue.parse(homepage.label)
.getValue(locale),
value: homepage.id || homepage['@id'],
}
)),
);
/**
* Return the IIIF v3 renderings of a manifest or null.
* @param {object} state
* @param {object} props
* @param {string} props.manifestId
* @param {string} props.windowId
* @returns {string|null}
*/
export const getManifestRenderings = createSelector(
[getManifestoInstance, getManifestLocale],
(manifest, locale) => manifest
&& manifest.getRenderings().map(rendering => (
{
label: rendering.getLabel().getValue(locale),
value: rendering.id,
}
)),
);
/**
* Return the IIIF v2/v3 seeAlso data from a manifest or null.
* @param {object} state
* @param {object} props
* @param {string} props.manifestId
* @param {string} props.windowId
* @returns {string|null}
*/
export const getManifestSeeAlso = createSelector(
[
getProperty('seeAlso'),
getManifestLocale,
],
(seeAlso, locale) => seeAlso
&& asArray(seeAlso).map(related => (
{
format: related.format,
label: PropertyValue.parse(related.label)
.getValue(locale),
value: related.id || related['@id'],
}
)),
);
/**
* Return the IIIF v2/v3 seeAlso data from a manifest or null.
* @param {object} state
* @param {object} props
* @param {string} props.manifestId
* @param {string} props.windowId
* @returns {string|null}
* @deprecated This does not actually return the content of "related" and
* might be removed in a future version.
* @see getManifestSeeAlso
*/
export const getManifestRelatedContent = getManifestSeeAlso;
/**
* Return the IIIF v2 realated links manifest or null
* @param {object} state
* @param {object} props
* @param {string} props.manifestId
* @param {string} props.windowId
* @returns {string|null}
*/
export const getManifestRelated = createSelector(
[
getProperty('related'),
getManifestLocale,
],
(relatedLinks, locale) => relatedLinks
&& asArray(relatedLinks).map(related => (
typeof related === 'string'
? {
value: related,
}
: {
format: related.format,
label: PropertyValue.parse(related.label)
.getValue(locale),
value: related.id || related['@id'],
}
)),
);
/**
* Return the IIIF requiredStatement (v3) or attribution (v2) data from a manifest or null
* @param {object} state
* @param {object} props
* @param {string} props.manifestId
* @param {string} props.windowId
* @returns {string|null}
*/
export const getRequiredStatement = createSelector(
[getManifestoInstance, getManifestLocale],
(manifest, locale) => manifest
&& asArray(manifest.getRequiredStatement())
.filter(l => l && l.getValues().some(v => v))
.map(labelValuePair => ({
label: (labelValuePair.label && labelValuePair.label.getValue(locale)) || null,
values: labelValuePair.getValues(locale),
})),
);
/**
* Return the IIIF v2 rights (v3) or license (v2) data from a manifest or null
* @param {object} state
* @param {object} props
* @param {string} props.manifestId
* @param {string} props.windowId
* @returns {string|null}
*/
export const getRights = createSelector(
[
getProperty('rights'),
getProperty('license'),
getManifestLocale,
],
(rights, license, locale) => {
const data = rights || license;
return asArray(PropertyValue.parse(data).getValues(locale));
},
);
/**
* Return the supplied thumbnail for a manifest or null.
* @param {object} state
* @param {object} props
* @param {string} props.manifestId
* @param {string} props.windowId
* @returns {string|null}
*/
export function getManifestThumbnail(state, props) {
const manifest = getManifestoInstance(state, props);
const thumbnailFactory = getThumbnailFactory(state, 80, 120);
if (!manifest) return undefined;
const thumbnail = thumbnailFactory.get(manifest);
return thumbnail && thumbnail.url;
}
/**
* Return manifest title.
* @param {object} state
* @param {object} props
* @param {string} props.manifestId
* @param {string} props.windowId
* @returns {string}
*/
export const getManifestTitle = createSelector(
[getManifestoInstance, getManifestLocale],
(manifest, locale) => manifest
&& manifest.getLabel().getValue(locale),
);
/**
* Return manifest description (IIIF v2) -- distinct from any description field nested under metadata.
* @param {object} state
* @param {object} props
* @param {string} props.manifestId
* @param {string} props.windowId
* @returns {string|null}
*/
export const getManifestDescription = createSelector(
[getLocale, getManifestoInstance],
(locale, manifest) => manifest
&& manifest.getDescription().getValue(locale),
);
/**
* Return manifest summary (IIIF v3).
* @param {object} state
* @param {object} props
* @param {string} props.manifestId
* @param {string} props.windowId
* @return {string|null}
*/
export const getManifestSummary = createSelector(
[
getProperty('summary'),
getManifestLocale,
],
(summary, locale) => summary
&& PropertyValue.parse(summary).getValue(locale),
);
/**
* Return manifest title.
* @param {object} state
* @param {object} props
* @param {string} props.manifestId
* @param {string} props.windowId
* @returns {string}
*/
export const getManifestUrl = createSelector(
[getManifestoInstance],
manifest => manifest && manifest.id,
);
/**
* Return metadata in a label / value structure
* This is a potential seam for pulling the i18n locale from
* state and plucking out the appropriate language.
* For now we're just getting the first.
* @param {object} Manifesto IIIF Resource (e.g. canvas, manifest)
* @param iiifResource
* @returns {Array[Object]}
*/
export function getDestructuredMetadata(iiifResource, locale = undefined) {
return (iiifResource
&& iiifResource.getMetadata().map(labelValuePair => ({
label: labelValuePair.getLabel(locale),
values: labelValuePair.getValues(locale),
}))
);
}
/**
* Return manifest metadata in a label / value structure
* @param {object} state
* @param {object} props
* @param {string} props.manifestId
* @param {string} props.windowId
* @returns {Array[Object]}
*/
export const getManifestMetadata = createSelector(
[getManifestoInstance, getManifestLocale],
(manifest, locale) => manifest && getDestructuredMetadata(manifest, locale),
);
/** */
function getLocalesForStructure(item) {
const languages = new Set([]);
/** Extract language indicators from IIIF v2 or v3 manifests */
const extractLanguage = (i) => {
if (!(i && typeof i === 'object')) return;
// IIIF v2 pattern
if (i['@language'] && i['@value']) {
languages.add(i['@language']);
return;
}
// IIIF v3 pattern
Object.keys(i).forEach((key) => {
languages.add(key);
});
};
if (Array.isArray(item)) {
item.forEach(i => extractLanguage(i));
} else {
extractLanguage(item);
}
return [...languages];
}
/** */
function getLocales(resource) {
if (!resource) return [];
const metadata = resource.getProperty('metadata') || [];
const languages = {};
for (let i = 0; i < metadata.length; i += 1) {
const item = metadata[i];
getLocalesForStructure(item.label).forEach((l) => { languages[l] = true; });
getLocalesForStructure(item.value).forEach((l) => { languages[l] = true; });
}
return Object.keys(languages);
}
export const getMetadataLocales = createSelector(
[getManifestoInstance],
manifest => getLocales(manifest),
);
/**
* Returns manifest search service.
* @param {object} state
* @param {object} props
* @param {string} props.manifestId
* @returns {string|null}
*/
export const getManifestSearchService = createSelector(
[getManifestoInstance],
(manifest) => {
if (!manifest) return null;
const searchService = manifest.getService('http://iiif.io/api/search/0/search')
|| manifest.getService('http://iiif.io/api/search/1/search');
if (searchService) return searchService;
return null;
},
);
/**
* Returns manifest autocomplete service.
* @param {object} state
* @param {object} props
* @param {string} props.manifestId
* @returns {string|null}
*/
export const getManifestAutocompleteService = createSelector(
[getManifestSearchService],
(searchService) => {
const autocompleteService = searchService && (
searchService.getService('http://iiif.io/api/search/0/autocomplete')
|| searchService.getService('http://iiif.io/api/search/1/autocomplete')
);
return autocompleteService && autocompleteService;
},
);