Skip to content

Commit

Permalink
Add 2 new props to set start canvas and time, and map them to start p…
Browse files Browse the repository at this point in the history
…roperty behavior from a IIIF Manifest (#322)

* Add 2 new props to set start canvas and time, and map them to start property behavior from a IIIF Manifest

* Flip precendence
  • Loading branch information
Dananji authored Dec 21, 2023
1 parent a026e03 commit b0d40ab
Show file tree
Hide file tree
Showing 7 changed files with 205 additions and 40 deletions.
8 changes: 7 additions & 1 deletion src/components/IIIFPlayer/IIIFPlayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export default function IIIFPlayer({
manifestUrl,
manifest,
customErrorMessage,
startCanvasId,
startCanvasTime,
children
}) {
if (!manifestUrl && !manifest)
Expand All @@ -22,7 +24,9 @@ export default function IIIFPlayer({
<IIIFPlayerWrapper
manifestUrl={manifestUrl}
manifest={manifest}
customErrorMessage={customErrorMessage}>
customErrorMessage={customErrorMessage}
startCanvasId={startCanvasId}
startCanvasTime={startCanvasTime}>
{children}
</IIIFPlayerWrapper>
</ErrorMessage>
Expand All @@ -36,6 +40,8 @@ IIIFPlayer.propTypes = {
manifestUrl: PropTypes.string,
manifest: PropTypes.object,
customErrorMessage: PropTypes.string,
startCanvasId: PropTypes.string,
startCanvasTime: PropTypes.number,
};

IIIFPlayer.defaultProps = {};
7 changes: 6 additions & 1 deletion src/components/IIIFPlayer/IIIFPlayer.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@ IIIFPlayer component, provides a wrapper consisting of the Context providers con
`IIIFPlayer` component accepts the following props;
- `manifestUrl` : accepts a URL of a manifest in the wild to be fetched
- `manifest` : accepts a JSON object representing data in a IIIF Manifest

** __Either `manifestUrl` or `manifest` is REQUIRED. If both props are given then `manifest` takes *precedence* over `manifestUrl`__

- `customErrorMessage`: accepts a messagge to display to the user in the unlikely event of the component crashing, this has a default error message and it is _not required_. The message can include HTML markup.
- `startCanvasId`: accepts a valid Canvas ID that exists within the given Manifest, this can specify the Canvas to show in Ramp on initialization. This can be mapped to the [`start` property](https://iiif.io/api/presentation/3.0/#start) in a IIIF Manifest.
- `startCanvasTime`: accepts a valid number for a time in seconds to start playback in the Canvas shown in Ramp on initialization.

** __Either `manifestUrl` or `manifest` are REQUIRED. If both props are given then `manifest` takes *precedence* over `manifestUrl`__
** __`startCanvasId` and `startCanvasTime` props takes *precedence* over the `start` property in a given IIIF Manifest. Defining either prop in the IIIFPlayer component overrides the `start` property in the IIIF Manifest.__

Import Ramp components individually and adjust the layout however you want. Play around with the code below.

Expand Down
32 changes: 25 additions & 7 deletions src/components/IIIFPlayerWrapper.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,30 @@
import React from 'react';
import { useManifestDispatch } from '../context/manifest-context';
import { usePlayerDispatch } from '../context/player-context';
import PropTypes from 'prop-types';
import { parseAutoAdvance } from '@Services/iiif-parser';
import { getCustomStart, parseAutoAdvance } from '@Services/iiif-parser';
import { getAnnotationService, getIsPlaylist } from '@Services/playlist-parser';
import { setAppErrorMessage, GENERIC_ERROR_MESSAGE } from '@Services/utility-helpers';
import { useErrorBoundary } from "react-error-boundary";

export default function IIIFPlayerWrapper({
manifestUrl,
customErrorMessage,
startCanvasId,
startCanvasTime,
children,
manifest: manifestValue,
}) {
const [manifest, setManifest] = React.useState(manifestValue);
const dispatch = useManifestDispatch();
const manifestDispatch = useManifestDispatch();
const playerDispatch = usePlayerDispatch();

const { showBoundary } = useErrorBoundary();

React.useEffect(() => {
setAppErrorMessage(customErrorMessage);
if (manifest) {
dispatch({ manifest: manifest, type: 'updateManifest' });
manifestDispatch({ manifest: manifest, type: 'updateManifest' });
} else {
let requestOptions = {
// NOTE: try thin in Avalon
Expand All @@ -37,7 +41,7 @@ export default function IIIFPlayerWrapper({
})
.then((data) => {
setManifest(data);
dispatch({ manifest: data, type: 'updateManifest' });
manifestDispatch({ manifest: data, type: 'updateManifest' });
})
.catch((error) => {
console.log('Error fetching manifest, ', error);
Expand All @@ -48,13 +52,25 @@ export default function IIIFPlayerWrapper({

React.useEffect(() => {
if (manifest) {
dispatch({ autoAdvance: parseAutoAdvance(manifest), type: "setAutoAdvance" });
manifestDispatch({ autoAdvance: parseAutoAdvance(manifest), type: "setAutoAdvance" });

const isPlaylist = getIsPlaylist(manifest);
dispatch({ isPlaylist: isPlaylist, type: 'setIsPlaylist' });
manifestDispatch({ isPlaylist: isPlaylist, type: 'setIsPlaylist' });

const annotationService = getAnnotationService(manifest);
dispatch({ annotationService: annotationService, type: 'setAnnotationService' });
manifestDispatch({ annotationService: annotationService, type: 'setAnnotationService' });

const customStart = getCustomStart(manifest, startCanvasId, startCanvasTime);
if (customStart.type == 'SR') {
playerDispatch({
currentTime: customStart.time,
type: 'setCurrentTime',
});
}
manifestDispatch({
canvasIndex: customStart.canvas,
type: 'switchCanvas',
});
}
}, [manifest]);

Expand All @@ -69,5 +85,7 @@ IIIFPlayerWrapper.propTypes = {
manifest: PropTypes.object,
customErrorMessage: PropTypes.string,
manifestUrl: PropTypes.string,
startCanvasId: PropTypes.string,
startCanvasTime: PropTypes.number,
children: PropTypes.node,
};
14 changes: 0 additions & 14 deletions src/components/StructuredNavigation/StructuredNavigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,20 +42,6 @@ const StructuredNavigation = () => {
structureItemsRef.current = structures;
manifestDispatch({ structures, type: 'setStructures' });
manifestDispatch({ timespans, type: 'setCanvasSegments' });
const customStart = getCustomStart(manifest);
if (!customStart) {
return;
}
if (customStart.type == 'SR') {
playerDispatch({
currentTime: customStart.time,
type: 'setCurrentTime',
});
}
manifestDispatch({
canvasIndex: customStart.canvas,
type: 'switchCanvas',
});
} catch (error) {
showBoundary(error);
}
Expand Down
85 changes: 68 additions & 17 deletions src/services/iiif-parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -297,38 +297,89 @@ export function getPlaceholderCanvas(manifest, canvasIndex, isPoster = false) {
}

/**
* Parse 'start' property in manifest if it is given
* Parse 'start' property in manifest if it is given, or use
* startCanvasId and startCanvasTime props in IIIFPlayer component
* to set the starting Canvas and time in Ramp on initialization
* In the spec there are 2 ways to specify 'start' property:
* https://iiif.io/api/presentation/3.0/#start
* Cookbook recipe for reference: https://iiif.io/api/cookbook/recipe/0015-start/
* @param {Object} manifest
* @param {String} startCanvasId from IIIFPlayer props
* @param {Number} startCanvasTime from IIIFPlayer props
* @returns {Object}
*/
export function getCustomStart(manifest) {
if (!parseManifest(manifest).getProperty('start')) {
return null;
export function getCustomStart(manifest, startCanvasId, startCanvasTime) {
let manifestStartProp = parseManifest(manifest).getProperty('start');
let startProp = {};
let currentCanvasIndex = 0;
// When none of the variable are set, return default values all set to zero
if (!manifestStartProp && startCanvasId === undefined && startCanvasTime === undefined) {
return { type: 'C', canvas: currentCanvasIndex, time: 0 };
} else if (startCanvasId != undefined || startCanvasTime != undefined) {
// Read user specified props from IIIFPlayer component
startProp = {
id: startCanvasId,
selector: { type: 'PointSelector', t: startCanvasTime === undefined ? 0 : startCanvasTime },
type: startCanvasTime === undefined ? 'Canvas' : 'SpecificResource'
};
// Set source property in the object for SpecificResource type
if (startCanvasTime != undefined) startProp.source = startCanvasId;
} else if (manifestStartProp) {
// Read 'start' property in Manifest when it exitsts
startProp = parseManifest(manifest).getProperty('start');
}
let currentCanvasIndex = null;
let startProp = parseManifest(manifest).getProperty('start');

let getCanvasIndex = (canvasId) => {
const canvases = canvasesInManifest(manifest);
const canvases = canvasesInManifest(manifest);
// Map given information in start property or user props to
// Canvas information in the given Manifest
let getCanvasInfo = (canvasId, time) => {
let startTime = time;
let currentIndex;

if (canvases != undefined && canvases?.length > 0) {
const currentCanvasIndex = canvases.findIndex((c) => {
return c.canvasId === canvasId;
});
return currentCanvasIndex;
if (canvasId === undefined) {
currentIndex = 0;
} else {
currentIndex = canvases.findIndex((c) => {
return c.canvasId === canvasId;
});
}
if (currentIndex === undefined || currentIndex < 0) {
console.error(
'iiif-parser -> getCustomStart() -> given canvas ID was not in Manifest, '
, startCanvasId
);
return { currentIndex: 0, startTime: 0 };
} else {
const currentCanvas = canvases[currentIndex];
if (currentCanvas.range != undefined) {
const { start, end } = currentCanvas.range;
if (!(time >= start && time <= end)) {
console.error(
'iiif-parser -> getCustomStart() -> given canvas start time is not within Canvas duration, '
, startCanvasTime
);
startTime = 0;
}
}
return { currentIndex, startTime };
}
} else {
console.error(
'iiif-parser -> getCustomStart() -> no Canvases in given Manifest'
);
return { currentIndex: 0, startTime: 0 };
}
};
if (startProp) {
if (startProp != undefined) {
switch (startProp.type) {
case 'Canvas':
currentCanvasIndex = getCanvasIndex(startProp.id);
return { type: 'C', canvas: currentCanvasIndex, time: 0 };
let canvasInfo = getCanvasInfo(startProp.id, 0);
return { type: 'C', canvas: canvasInfo.currentIndex, time: canvasInfo.startTime };
case 'SpecificResource':
currentCanvasIndex = getCanvasIndex(startProp.source);
let customStart = startProp.selector.t;
return { type: 'SR', canvas: currentCanvasIndex, time: customStart };
canvasInfo = getCanvasInfo(startProp.source, customStart);
return { type: 'SR', canvas: canvasInfo.currentIndex, time: canvasInfo.startTime };
}
}
}
Expand Down
80 changes: 80 additions & 0 deletions src/services/iiif-parser.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import manifestWoStructure from '@TestData/transcript-canvas';
import singleSrcManifest from '@TestData/transcript-multiple-canvas';
import autoAdvanceManifest from '@TestData/multiple-canvas-auto-advance';
import playlistManifest from '@TestData/playlist';
import emptyManifest from '@TestData/empty-manifest';
import * as iiifParser from './iiif-parser';
import * as util from './utility-helpers';

Expand All @@ -25,6 +26,11 @@ describe('iiif-parser', () => {
expect(canvases[1].canvasId).toEqual('https://example.com/sample/transcript-annotation/canvas/2');
expect(canvases[1].isEmpty).toBeTruthy();
});

it('returns empty list for empty Manifest', () => {
const canvases = iiifParser.canvasesInManifest(emptyManifest);
expect(canvases).toHaveLength(0);
});
});

describe('manifestCanvasesInfo()', () => {
Expand Down Expand Up @@ -296,6 +302,80 @@ describe('iiif-parser', () => {
expect(customStart.canvas).toEqual(0);
});
});

it('returns default values when start property is not defined in Manifest', () => {
const customStart = iiifParser.getCustomStart(manifestWoStructure);
expect(customStart.type).toEqual('C');
expect(customStart.time).toEqual(0);
expect(customStart.canvas).toEqual(0);
});

it('returns values related to given start canvas ID', () => {
const customStart = iiifParser.getCustomStart(
playlistManifest, 'http://example.com/manifests/playlist/canvas/2'
);
expect(customStart.type).toEqual('C');
expect(customStart.time).toEqual(0);
expect(customStart.canvas).toEqual(1);
});

it('returns values related to given start canvas time', () => {
const customStart = iiifParser.getCustomStart(manifestWoStructure, undefined, 23);
expect(customStart.type).toEqual('SR');
expect(customStart.time).toEqual(23);
expect(customStart.canvas).toEqual(0);
});

it('returns values related to given start canvas ID and time', () => {
const customStart = iiifParser.getCustomStart(
playlistManifest, 'http://example.com/manifests/playlist/canvas/3', 233
);
expect(customStart.type).toEqual('SR');
expect(customStart.time).toEqual(233);
expect(customStart.canvas).toEqual(2);
});

it('returns zero as start time when given value is outside of Canvas duration', () => {
// Mock console.error function
let originalError = console.error;
console.error = jest.fn();
const customStart = iiifParser.getCustomStart(
playlistManifest, 'http://example.com/manifests/playlist/canvas/3', 653
);
expect(customStart.type).toEqual('SR');
expect(customStart.time).toEqual(0);
expect(customStart.canvas).toEqual(2);
expect(console.error).toBeCalledTimes(1);
console.error = originalError;
});

it('returns zero as current canvas index when given ID is not in the Manifest', () => {
// Mock console.error function
let originalError = console.error;
console.error = jest.fn();
const customStart = iiifParser.getCustomStart(
playlistManifest, 'http://example.com/manifests/playlist/canvas/33', 653
);
expect(customStart.type).toEqual('SR');
expect(customStart.time).toEqual(0);
expect(customStart.canvas).toEqual(0);
expect(console.error).toBeCalledTimes(1);
console.error = originalError;
});

it('return default values with empty manifest', () => {
// Mock console.error function
let originalError = console.error;
console.error = jest.fn();
const customStart = iiifParser.getCustomStart(
emptyManifest, 'http://example.com/manifests/playlist/canvas/33', 653
);
expect(customStart.type).toEqual('SR');
expect(customStart.time).toEqual(0);
expect(customStart.canvas).toEqual(0);
expect(console.error).toBeCalledTimes(1);
console.error = originalError;
});
});

describe('getRenderingFiles()', () => {
Expand Down
19 changes: 19 additions & 0 deletions src/test_data/empty-manifest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export default {
'@context': [
'http://www.w3.org/ns/anno.jsonld',
'http://iiif.io/api/presentation/3/context.json',
],
type: 'Manifest',
id: 'https://example.com/manifest/empty-manifest',
label: {
en: ['Beginning Responsibility: Lunchroom Manners'],
},
metadata: [],
items: [],
thumbnail: [
{
id: 'https://example.com/manifest/thumbnail/lunchroom_manners_poster.jpg',
type: 'Image',
},
],
};

0 comments on commit b0d40ab

Please sign in to comment.