Skip to content
Snippets Groups Projects
Commit 2db0a7a0 authored by Jack Reed's avatar Jack Reed
Browse files

Add additional support for AnnotationItem

parent c3c1c1d2
Branches
No related tags found
No related merge requests found
...@@ -269,6 +269,33 @@ ...@@ -269,6 +269,33 @@
] ]
} }
], ],
"annotations": [
{
"id": "https://example.org/iiif/foo/bar/annopage/",
"type": "AnnotationPage",
"items": [
{
"id": "https://example.org/iiif/book1/page/manifest/a1",
"type": "Annotation",
"motivation": "commenting",
"body": {
"type": "TextualBody",
"language": "en",
"value": "I love this!"
},
"target": "https://iiif.bodleian.ox.ac.uk/iiif/canvas/9cca8fdd-4a61-4429-8ac1-f648764b4d6d.json#xywh=2000,500,2000,2000"
}
]
},
{
"id": "https://example.org/iiif/foo/bar/annopage/remote/1",
"type": "AnnotationPage"
},
{
"id": "https://example.org/iiif/foo/bar/annopage/remote/2",
"type": "AnnotationPage"
}
],
"metadata": [ "metadata": [
{ {
"label": { "label": {
......
import AnnotationItem from '../../../src/lib/AnnotationItem';
describe('AnnotationItem', () => {
describe('id', () => {
it('returns the id', () => {
expect(new AnnotationItem({ id: 'foo' }).id).toEqual('foo');
});
it('creates a memoized uuid', () => {
const annoResource = new AnnotationItem();
const expected = annoResource.id;
expect(annoResource.id).toEqual(expected);
});
});
describe('targetId', () => {
it('removes fragmentSelector coords from string targets', () => {
expect(
new AnnotationItem({ target: 'www.example.com/#xywh=10,10,100,200' }).targetId,
).toEqual('www.example.com/');
});
it('returns null when there is no target', () => {
expect(new AnnotationItem().targetId).toBeNull();
});
});
describe('motivations', () => {
it('with no motivation', () => {
expect(new AnnotationItem().motivations).toEqual([]);
});
it('with a single motivation', () => {
expect(new AnnotationItem({ motivation: 'commenting' })
.motivations).toEqual(['commenting']);
});
it('with multiple motivations', () => {
expect(new AnnotationItem({ motivation: ['commenting', 'funstuff'] })
.motivations).toEqual(['commenting', 'funstuff']);
});
});
describe('resources/body', () => {
it('with no body', () => {
expect(new AnnotationItem().resources).toEqual([]);
expect(new AnnotationItem().body).toEqual([]);
});
it('with a single body', () => {
expect(new AnnotationItem({ body: 'foo' })
.resources).toEqual(['foo']);
expect(new AnnotationItem({ body: 'foo' })
.body).toEqual(['foo']);
});
it('with multiple bodies', () => {
expect(new AnnotationItem({ body: ['foo', 'bar'] })
.resources).toEqual(['foo', 'bar']);
expect(new AnnotationItem({ body: ['foo', 'bar'] })
.body).toEqual(['foo', 'bar']);
});
});
describe('target', () => {
it('with no target', () => {
expect(new AnnotationItem().target).toEqual([]);
});
it('with a single target', () => {
expect(new AnnotationItem({ target: 'foo' })
.target).toEqual(['foo']);
});
it('with multiple target', () => {
expect(new AnnotationItem({ target: ['foo', 'bar'] })
.target).toEqual(['foo', 'bar']);
});
});
describe('selector', () => {
it('returns the on string (for simple fragment selector)', () => {
expect(new AnnotationItem({ target: 'yolo' }).selector).toEqual('yolo');
});
});
describe('chars', () => {
it('with no resource', () => {
expect(new AnnotationItem().chars).toEqual('');
});
it('with a single body', () => {
expect(new AnnotationItem({ body: { value: 'foo' } })
.chars).toEqual('foo');
});
it('with multiple bodies', () => {
expect(new AnnotationItem({ body: [{ value: 'foo' }, { value: 'bar' }] })
.chars).toEqual('foo bar');
});
});
describe('fragmentSelector', () => {
it('simple string', () => {
expect(new AnnotationItem({ target: 'www.example.com/#xywh=10,10,100,200' })
.fragmentSelector).toEqual([10, 10, 100, 200]);
});
});
});
import AnnotationPage from '../../../src/lib/AnnotationPage'; import AnnotationPage from '../../../src/lib/AnnotationPage';
import AnnotationItem from '../../../src/lib/AnnotationItem';
describe('AnnotationPage', () => { describe('AnnotationPage', () => {
describe('id', () => { describe('id', () => {
...@@ -17,16 +18,16 @@ describe('AnnotationPage', () => { ...@@ -17,16 +18,16 @@ describe('AnnotationPage', () => {
}); });
describe('items', () => { describe('items', () => {
it('returns items', () => { it('returns items', () => {
expect(new AnnotationPage( new AnnotationPage(
{ items: [{ foo: 'bar' }] }, { items: [{ foo: 'bar' }] },
).items).toEqual([{ foo: 'bar' }]); ).items.forEach(resource => expect(resource).toBeInstanceOf(AnnotationItem));
}); });
}); });
describe('resources', () => { describe('resources', () => {
it('returns items', () => { it('returns items', () => {
expect(new AnnotationPage( new AnnotationPage(
{ items: [{ foo: 'bar' }] }, { items: [{ foo: 'bar' }] },
).resources).toEqual([{ foo: 'bar' }]); ).items.forEach(resource => expect(resource).toBeInstanceOf(AnnotationItem));
}); });
}); });
}); });
...@@ -48,6 +48,27 @@ describe('ManifestoCanvas', () => { ...@@ -48,6 +48,27 @@ describe('ManifestoCanvas', () => {
); );
}); });
}); });
describe('processAnnotations', () => {
describe('v2', () => {
it('fetches annotations for each annotationList', () => {
const otherContentInstance = new ManifestoCanvas(
manifesto.create(otherContentFixture).getSequences()[0].getCanvases()[0],
);
const fetchMock = jest.fn();
otherContentInstance.processAnnotations(fetchMock);
expect(fetchMock).toHaveBeenCalledTimes(1);
});
});
describe('v3', () => {
it('fetches annotations for external items and receives annotations for items that are embedded', () => {
const receiveMock = jest.fn();
const fetchMock = jest.fn();
v3Instance.processAnnotations(fetchMock, receiveMock);
expect(receiveMock).toHaveBeenCalledTimes(1);
expect(fetchMock).toHaveBeenCalledTimes(2);
});
});
});
describe('imageInformationUri', () => { describe('imageInformationUri', () => {
it('correctly returns an image information url for a v2 Image API', () => { it('correctly returns an image information url for a v2 Image API', () => {
expect(instance.imageInformationUri).toEqual('https://stacks.stanford.edu/image/iiif/hg676jb4964%2F0380_796-44/info.json'); expect(instance.imageInformationUri).toEqual('https://stacks.stanford.edu/image/iiif/hg676jb4964%2F0380_796-44/info.json');
......
...@@ -26,7 +26,9 @@ export class WindowViewer extends Component { ...@@ -26,7 +26,9 @@ export class WindowViewer extends Component {
* Request the initial canvas on mount * Request the initial canvas on mount
*/ */
componentDidMount() { componentDidMount() {
const { currentCanvases, fetchInfoResponse, fetchAnnotation } = this.props; const {
currentCanvases, fetchInfoResponse, fetchAnnotation, receiveAnnotation,
} = this.props;
if (!this.infoResponseIsInStore()) { if (!this.infoResponseIsInStore()) {
currentCanvases.forEach((canvas) => { currentCanvases.forEach((canvas) => {
...@@ -35,9 +37,7 @@ export class WindowViewer extends Component { ...@@ -35,9 +37,7 @@ export class WindowViewer extends Component {
if (imageResource) { if (imageResource) {
fetchInfoResponse({ imageResource }); fetchInfoResponse({ imageResource });
} }
manifestoCanvas.annotationListUris.forEach((uri) => { manifestoCanvas.processAnnotations(fetchAnnotation, receiveAnnotation);
fetchAnnotation(manifestoCanvas.canvas.id, uri);
});
}); });
} }
} }
...@@ -48,7 +48,7 @@ export class WindowViewer extends Component { ...@@ -48,7 +48,7 @@ export class WindowViewer extends Component {
*/ */
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
const { const {
currentCanvasId, currentCanvases, view, fetchInfoResponse, fetchAnnotation, currentCanvasId, currentCanvases, view, fetchInfoResponse, fetchAnnotation, receiveAnnotation,
} = this.props; } = this.props;
if (prevProps.view !== view if (prevProps.view !== view
...@@ -60,9 +60,7 @@ export class WindowViewer extends Component { ...@@ -60,9 +60,7 @@ export class WindowViewer extends Component {
if (imageResource) { if (imageResource) {
fetchInfoResponse({ imageResource }); fetchInfoResponse({ imageResource });
} }
manifestoCanvas.annotationListUris.forEach((uri) => { manifestoCanvas.processAnnotations(fetchAnnotation, receiveAnnotation);
fetchAnnotation(manifestoCanvas.canvas.id, uri);
});
}); });
} }
} }
...@@ -140,6 +138,7 @@ WindowViewer.propTypes = { ...@@ -140,6 +138,7 @@ WindowViewer.propTypes = {
fetchAnnotation: PropTypes.func.isRequired, fetchAnnotation: PropTypes.func.isRequired,
fetchInfoResponse: PropTypes.func.isRequired, fetchInfoResponse: PropTypes.func.isRequired,
infoResponses: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types infoResponses: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
receiveAnnotation: PropTypes.func.isRequired,
view: PropTypes.string.isRequired, view: PropTypes.string.isRequired,
windowId: PropTypes.string.isRequired, windowId: PropTypes.string.isRequired,
}; };
...@@ -14,7 +14,7 @@ import { AnnotationSettings } from '../components/AnnotationSettings'; ...@@ -14,7 +14,7 @@ import { AnnotationSettings } from '../components/AnnotationSettings';
*/ */
const mapStateToProps = (state, { windowId }) => ({ const mapStateToProps = (state, { windowId }) => ({
displayAll: getWindow(state, { windowId }).displayAllAnnotations, displayAll: getWindow(state, { windowId }).displayAllAnnotations,
displayAllDisabled: getAnnotationResourcesByMotivation(state, { motivations: ['oa:commenting', 'sc:painting'], windowId }).length < 2, displayAllDisabled: getAnnotationResourcesByMotivation(state, { motivations: ['oa:commenting', 'sc:painting', 'commenting'], windowId }).length < 2,
}); });
/** /**
......
...@@ -28,7 +28,7 @@ const mapStateToProps = (state, { canvasId, windowId }) => ({ ...@@ -28,7 +28,7 @@ const mapStateToProps = (state, { canvasId, windowId }) => ({
allAnnotationsAreHighlighted: getWindow(state, { windowId }).displayAllAnnotations, allAnnotationsAreHighlighted: getWindow(state, { windowId }).displayAllAnnotations,
annotations: getIdAndContentOfResources( annotations: getIdAndContentOfResources(
getAnnotationResourcesByMotivationForCanvas( getAnnotationResourcesByMotivationForCanvas(
state, { canvasId, motivations: ['oa:commenting', 'sc:painting'], windowId }, state, { canvasId, motivations: ['oa:commenting', 'sc:painting', 'commenting'], windowId },
), ),
), ),
label: getCanvasLabel(state, { label: getCanvasLabel(state, {
......
...@@ -15,7 +15,7 @@ import { WindowSideBarAnnotationsPanel } from '../components/WindowSideBarAnnota ...@@ -15,7 +15,7 @@ import { WindowSideBarAnnotationsPanel } from '../components/WindowSideBarAnnota
* @private * @private
*/ */
const mapStateToProps = (state, { windowId }) => ({ const mapStateToProps = (state, { windowId }) => ({
annotationCount: getAnnotationResourcesByMotivation(state, { motivations: ['oa:commenting', 'sc:painting'], windowId }).length, annotationCount: getAnnotationResourcesByMotivation(state, { motivations: ['oa:commenting', 'sc:painting', 'commenting'], windowId }).length,
selectedCanvases: getVisibleCanvases(state, { windowId }), selectedCanvases: getVisibleCanvases(state, { windowId }),
}); });
......
...@@ -32,7 +32,7 @@ const mapDispatchToProps = (dispatch, { windowId }) => ({ ...@@ -32,7 +32,7 @@ const mapDispatchToProps = (dispatch, { windowId }) => ({
* @private * @private
*/ */
const mapStateToProps = (state, { windowId }) => ({ const mapStateToProps = (state, { windowId }) => ({
hasAnnotations: getAnnotationResourcesByMotivation(state, { motivations: ['oa:commenting', 'sc:painting'], windowId }).length > 0, hasAnnotations: getAnnotationResourcesByMotivation(state, { motivations: ['oa:commenting', 'sc:painting', 'commenting'], windowId }).length > 0,
hasSearchResults: getWindow(state, { windowId }).suggestedSearches || getSearchQuery(state, { hasSearchResults: getWindow(state, { windowId }).suggestedSearches || getSearchQuery(state, {
companionWindowId: (getCompanionWindowsForPosition(state, { position: 'left', windowId })[0] || {}).id, companionWindowId: (getCompanionWindowsForPosition(state, { position: 'left', windowId })[0] || {}).id,
windowId, windowId,
......
...@@ -27,6 +27,7 @@ const mapStateToProps = (state, { windowId }) => ( ...@@ -27,6 +27,7 @@ const mapStateToProps = (state, { windowId }) => (
const mapDispatchToProps = { const mapDispatchToProps = {
fetchAnnotation: actions.fetchAnnotation, fetchAnnotation: actions.fetchAnnotation,
fetchInfoResponse: actions.fetchInfoResponse, fetchInfoResponse: actions.fetchInfoResponse,
receiveAnnotation: actions.receiveAnnotation,
}; };
......
import compact from 'lodash/compact';
import flatten from 'lodash/flatten';
import uuid from 'uuid/v4';
/**
* A modeled WebAnnotation item
*/
export default class AnnotationItem {
/** */
constructor(resource = {}) {
this.resource = resource;
}
/** */
get id() {
this._id = this._id || this.resource.id || uuid(); // eslint-disable-line no-underscore-dangle
return this._id; // eslint-disable-line no-underscore-dangle
}
/** */
get targetId() {
const target = this.target[0];
switch (typeof target) {
case 'string':
return target.replace(/#?xywh=(.*)$/, '');
default:
return null;
}
}
/**
* @return {[Array]}
*/
get motivations() {
return flatten(compact(new Array(this.resource.motivation)));
}
/** */
get body() {
return flatten(compact(new Array(this.resource.body)));
}
/** */
get resources() {
return this.body;
}
/** */
get target() {
return flatten(compact(new Array(this.resource.target)));
}
/** */
get chars() {
return this.body.map(r => r.value).join(' ');
}
/** */
get selector() {
const target = this.target[0];
switch (typeof target) {
case 'string':
return target;
default:
return null;
}
}
/** */
get fragmentSelector() {
const { selector } = this;
switch (typeof selector) {
case 'string':
return selector.match(/xywh=(.*)$/)[1].split(',').map(str => parseInt(str, 10));
default:
return null;
}
}
}
import flatten from 'lodash/flatten';
import AnnotationItem from './AnnotationItem';
/** /**
* Annotation representation for IIIF Presentation v3 * Annotation representation for IIIF Presentation v3
* https://iiif.io/api/presentation/3.0/#55-annotation-page * https://iiif.io/api/presentation/3.0/#55-annotation-page
...@@ -24,7 +26,7 @@ export default class AnnotationPage { ...@@ -24,7 +26,7 @@ export default class AnnotationPage {
get items() { get items() {
if (!this.json || !this.json.items) return []; if (!this.json || !this.json.items) return [];
return this.json.items; return flatten([this.json.items]).map(resource => new AnnotationItem(resource));
} }
/** /**
......
...@@ -43,6 +43,32 @@ export default class ManifestoCanvas { ...@@ -43,6 +43,32 @@ export default class ManifestoCanvas {
.map(otherContent => otherContent['@id']); .map(otherContent => otherContent['@id']);
} }
/** */
get canvasAnnotationPages() {
return flatten(
new Array(this.canvas.__jsonld.annotations), // eslint-disable-line no-underscore-dangle
)
.filter(annotations => annotations && annotations.type === 'AnnotationPage');
}
/** */
processAnnotations(fetchAnnotation, receiveAnnotation) {
// IIIF v2
this.annotationListUris.forEach((uri) => {
fetchAnnotation(this.canvas.id, uri);
});
// IIIF v3
this.canvasAnnotationPages.forEach((annotation) => {
// If there are no items, try to retrieve the referenced resource.
// otherwise the resource should be embedded and just add to the store.
if (!annotation.items) {
fetchAnnotation(this.canvas.id, annotation.id);
} else {
receiveAnnotation(this.canvas.id, annotation.id, annotation);
}
});
}
/** /**
* Will negotiate a v2 or v3 type of resource * Will negotiate a v2 or v3 type of resource
*/ */
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment