From 2599fa595df094b3c91a223a01fc15fc75cc836e Mon Sep 17 00:00:00 2001 From: Benjamin Armintor Date: Tue, 28 Jan 2025 23:52:26 -0500 Subject: [PATCH] COLUMBIA: Core mirador behaviors to provide a plugin target for text resources - refactor type-based filters into a module - MiradorCanvas.imagesResources does not assume any service is an image service - stub TextViewer shows empty div, source elements for text resources, and canvas navigation - fixes https://github.com/ProjectMirador/mirador/issues/4085 --- __tests__/src/lib/resourceFilters.test.js | 31 ++++++++++ src/lib/MiradorCanvas.js | 39 +++++++------ src/lib/canvasTypes.js | 22 +++++++ src/lib/resourceFilters.js | 70 +++++++++++++++++++++++ src/lib/serviceProfiles.js | 13 +++++ 5 files changed, 156 insertions(+), 19 deletions(-) create mode 100644 __tests__/src/lib/resourceFilters.test.js create mode 100644 src/lib/canvasTypes.js create mode 100644 src/lib/resourceFilters.js create mode 100644 src/lib/serviceProfiles.js diff --git a/__tests__/src/lib/resourceFilters.test.js b/__tests__/src/lib/resourceFilters.test.js new file mode 100644 index 000000000..e27e568cc --- /dev/null +++ b/__tests__/src/lib/resourceFilters.test.js @@ -0,0 +1,31 @@ +import { Utils } from 'manifesto.js'; +import flattenDeep from 'lodash/flattenDeep'; +import manifestFixture019 from '../../fixtures/version-2/019.json'; +import { + filterByProfiles, filterByTypes, +} from '../../../src/lib/resourceFilters'; + +describe('resourceFilters', () => { + let canvas; + beforeEach(() => { + [canvas] = Utils.parseManifest(manifestFixture019).getSequences()[0].getCanvases(); + }); + describe('filterByProfiles', () => { + it('filters resources', () => { + const services = flattenDeep(canvas.resourceAnnotations.map((a) => a.getResource().getServices())); + expect(filterByProfiles(services, 'http://iiif.io/api/image/2/level2.json').map((s) => s.id)).toEqual([ + 'https://stacks.stanford.edu/image/iiif/hg676jb4964%2F0380_796-44', + ]); + expect(filterByProfiles(services, 'http://nonexistent.io/api/service.json').map((s) => s.id)).toEqual([]); + }); + }); + describe('filterByTypes', () => { + it('filters resources', () => { + const resources = flattenDeep(canvas.resourceAnnotations.map((a) => a.getResource())); + expect(filterByTypes(resources, 'dctypes:Image').map((r) => r.id)).toEqual([ + 'https://stacks.stanford.edu/image/iiif/hg676jb4964%2F0380_796-44/full/full/0/default.jpg', + ]); + expect(filterByTypes(resources, 'Nonexistent').map((r) => r.id)).toEqual([]); + }); + }); +}); diff --git a/src/lib/MiradorCanvas.js b/src/lib/MiradorCanvas.js index a0184fefe..669f00cda 100644 --- a/src/lib/MiradorCanvas.js +++ b/src/lib/MiradorCanvas.js @@ -1,6 +1,12 @@ import flatten from 'lodash/flatten'; import flattenDeep from 'lodash/flattenDeep'; import { Canvas, AnnotationPage, Annotation } from 'manifesto.js'; +import { + audioResourcesFrom, choiceResourcesFrom, hasImageService, imageResourcesFrom, iiifImageResourcesFrom, + textResourcesFrom, videoResourcesFrom, +} from './resourceFilters'; +import canvasTypes from './canvasTypes'; + /** * MiradorCanvas - adds additional, testable logic around Manifesto's Canvas * https://iiif-commons.github.io/manifesto/classes/_canvas_.manifesto.canvas.html @@ -68,7 +74,8 @@ export default class MiradorCanvas { get imageResources() { const resources = flattenDeep([ this.canvas.getImages().map(i => i.getResource()), - this.canvas.getContent().map(i => i.getBody()), + imageResourcesFrom(this.contentBodies), + choiceResourcesFrom(this.contentBodies), ]); return flatten(resources.map((resource) => { @@ -82,35 +89,30 @@ export default class MiradorCanvas { } /** */ - get textResources() { - const resources = flattenDeep([ + get contentBodies() { + return flattenDeep([ this.canvas.getContent().map(i => i.getBody()), ]); - return flatten(resources.filter((resource) => resource.getProperty('type') === 'Text')); + } + + /** */ + get textResources() { + return textResourcesFrom(this.contentBodies); } /** */ get videoResources() { - const resources = flattenDeep([ - this.canvas.getContent().map(i => i.getBody()), - ]); - return flatten(resources.filter((resource) => resource.getProperty('type') === 'Video')); + return flatten(videoResourcesFrom(this.contentBodies)); } /** */ get audioResources() { - const resources = flattenDeep([ - this.canvas.getContent().map(i => i.getBody()), - ]); - - return flatten(resources.filter((resource) => resource.getProperty('type') === 'Sound')); + return flatten(audioResourcesFrom(this.contentBodies)); } /** */ get v2VttContent() { - const resources = flattenDeep([ - this.canvas.getContent().map(i => i.getBody()), - ]); + const resources = this.contentBodies; return flatten(resources.filter((resource) => resource.getProperty('format') === 'text/vtt')); } @@ -166,13 +168,12 @@ export default class MiradorCanvas { /** */ get iiifImageResources() { - return this.imageResources - .filter(r => r && r.getServices()[0] && r.getServices()[0].id); + return iiifImageResourcesFrom(this.imageResources); } /** */ get imageServiceIds() { - return this.iiifImageResources.map(r => r.getServices()[0].id); + return this.iiifImageResources.map(hasImageService); } /** diff --git a/src/lib/canvasTypes.js b/src/lib/canvasTypes.js new file mode 100644 index 000000000..71c9d24fe --- /dev/null +++ b/src/lib/canvasTypes.js @@ -0,0 +1,22 @@ +/** values for type/@type that indicate an image content resource */ +const imageTypes = ['Image', 'StillImage', 'dctypes:Image', 'dctypes:StillImage']; + +/** values for type/@type that indicate a sound content resource */ +const audioTypes = ['Audio', 'Sound', 'dctypes:Audio', 'dctypes:Sound']; + +/** values for type/@type that indicate a choice resource */ +const choiceTypes = ['oa:Choice']; + +/** values for type/@type that indicate a text content resource */ +const textTypes = ['Document', 'Text', 'dctypes:Document', 'dctypes:Text']; + +/** values for type/@type that indicate a video content resource */ +const videoTypes = ['Video', 'MovingImage', 'dctypes:Video', 'dctypes:MovingImage']; + +export default { + audioTypes, + choiceTypes, + imageTypes, + textTypes, + videoTypes, +}; diff --git a/src/lib/resourceFilters.js b/src/lib/resourceFilters.js new file mode 100644 index 000000000..16e23f7a4 --- /dev/null +++ b/src/lib/resourceFilters.js @@ -0,0 +1,70 @@ +import canvasTypes from './canvasTypes'; +import serviceProfiles from './serviceProfiles'; + +/** + * Filter resources by profile property in given profiles + */ +export function filterByProfiles(resources, profiles) { + if (profiles === undefined || resources === undefined) return []; + + if (!Array.isArray(profiles)) { + return resources.filter((resource) => profiles === resource.getProperty('profile')); + } + + return resources.filter((resource) => profiles.includes(resource.getProperty('profile'))); +} + +/** + * Filter resources by type property in given types + */ +export function filterByTypes(resources, types) { + if (types === undefined || resources === undefined) return []; + + if (!Array.isArray(types)) { + return resources.filter((resource) => types === resource.getProperty('type')); + } + + return resources.filter((resource) => types.includes(resource.getProperty('type'))); +} + +/** */ +export function audioResourcesFrom(resources) { + return filterByTypes(resources, canvasTypes.audioTypes); +} + +/** */ +export function choiceResourcesFrom(resources) { + return filterByTypes(resources, canvasTypes.choiceTypes); +} + +/** + */ +export function imageServicesFrom(services) { + return filterByProfiles(services, serviceProfiles.iiifImageProfiles); +} + +/** */ +export function hasImageService(resource) { + const imageServices = imageServicesFrom(resource ? resource.getServices() : []); + return imageServices[0] && imageServices[0].id; +} + +/** */ +export function iiifImageResourcesFrom(resources) { + return imageResourcesFrom(resources).filter((r) => hasImageService(r)); +} + +/** */ +export function imageResourcesFrom(resources) { + return filterByTypes(resources, canvasTypes.imageTypes); +} + +/** */ +export function textResourcesFrom(resources) { + return filterByTypes(resources, canvasTypes.textTypes); +} + +/** */ +export function videoResourcesFrom(resources) { + return filterByTypes(resources, canvasTypes.videoTypes); +} diff --git a/src/lib/serviceProfiles.js b/src/lib/serviceProfiles.js new file mode 100644 index 000000000..c73f95665 --- /dev/null +++ b/src/lib/serviceProfiles.js @@ -0,0 +1,13 @@ +/** values for profile that indicate an image service */ +const iiifImageProfiles = [ + 'level2', + 'level1', + 'level0', + 'http://iiif.io/api/image/2/level2.json', + 'http://iiif.io/api/image/2/level1.json', + 'http://iiif.io/api/image/2/level0.json', +]; + +export default { + iiifImageProfiles, +};