UNPKG

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.

434 lines (393 loc) 13.1 kB
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)) || 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], (manifest) => manifest && asArray(manifest.getRequiredStatement()).flatMap((labelValuePair) => { const values = labelValuePair?.getValues() ?? []; if (!values.length) { return []; } const labels = labelValuePair?.getLabels() ?? []; return Array.from({ length: Math.max(labels.length, values.length) }, (_, idx) => idx) .filter((idx) => values[idx]) .map((idx) => ({ label: labels[idx] ?? null, values: [values[idx]], })); }), ); /** * 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; });