diff --git a/__tests__/src/components/OpenSeadragonViewer.test.js b/__tests__/src/components/OpenSeadragonViewer.test.js index 6c4e66698e7a8def24169fef778a8145838ece62..cce744544cc69697dac7491a23c8972e1c6dfa56 100644 --- a/__tests__/src/components/OpenSeadragonViewer.test.js +++ b/__tests__/src/components/OpenSeadragonViewer.test.js @@ -4,6 +4,7 @@ import OpenSeadragon from 'openseadragon'; import { OpenSeadragonViewer } from '../../../src/components/OpenSeadragonViewer'; import OpenSeadragonCanvasOverlay from '../../../src/lib/OpenSeadragonCanvasOverlay'; import Annotation from '../../../src/lib/Annotation'; +import CanvasWorld from '../../../src/lib/CanvasWorld'; jest.mock('openseadragon'); jest.mock('../../../src/lib/OpenSeadragonCanvasOverlay'); @@ -34,6 +35,7 @@ describe('OpenSeadragonViewer', () => { updateViewport={updateViewport} t={k => k} classes={{ controls: 'controls' }} + canvasWorld={new CanvasWorld([])} > <div className="foo" /> </OpenSeadragonViewer>, @@ -108,6 +110,7 @@ describe('OpenSeadragonViewer', () => { viewer={{ x: 1, y: 0, zoom: 0.5 }} config={{}} updateViewport={updateViewport} + canvasWorld={new CanvasWorld([])} t={k => k} > <div className="foo" /> diff --git a/__tests__/src/components/WindowViewer.test.js b/__tests__/src/components/WindowViewer.test.js index f66019f014df5d493b0b4f0457cf2b0191697231..1fc591a9bb0d30bab47b8aef2dc71735704cf8a9 100644 --- a/__tests__/src/components/WindowViewer.test.js +++ b/__tests__/src/components/WindowViewer.test.js @@ -8,7 +8,7 @@ import fixture from '../../fixtures/version-2/019.json'; import emptyCanvasFixture from '../../fixtures/version-2/emptyCanvas.json'; import otherContentFixture from '../../fixtures/version-2/299843.json'; -let canvases = manifesto.create(fixture).getSequences()[0].getCanvases(); +let currentCanvases = manifesto.create(fixture).getSequences()[0].getCanvases(); let mockWindow = { canvasIndex: 0, @@ -23,7 +23,7 @@ function createWrapper(props) { infoResponses={{}} fetchInfoResponse={() => {}} fetchAnnotation={() => {}} - canvases={canvases} + currentCanvases={[currentCanvases[1]]} window={mockWindow} {...props} />, @@ -32,10 +32,8 @@ function createWrapper(props) { describe('WindowViewer', () => { let wrapper; - beforeEach(() => { - wrapper = createWrapper(); - }); it('renders properly', () => { + wrapper = createWrapper(); expect(wrapper.matchesElement( <> <OSDViewer> @@ -44,31 +42,6 @@ describe('WindowViewer', () => { </>, )).toBe(true); }); - describe('currentCanvases', () => { - it('by default returns a single canvas', () => { - expect(wrapper.instance().currentCanvases().length).toEqual(1); - }); - describe('book view', () => { - it('when the first canvas is selected', () => { - wrapper = createWrapper({ - window: { - canvasIndex: 0, - view: 'book', - }, - }); - expect(wrapper.instance().currentCanvases().length).toEqual(1); - }); - it('when the second canvas is selected', () => { - wrapper = createWrapper({ - window: { - canvasIndex: 1, - view: 'book', - }, - }); - expect(wrapper.instance().currentCanvases().length).toEqual(2); - }); - }); - }); describe('currentInfoResponses', () => { describe('returns only available infoResponses', () => { it('isFetching is false', () => { @@ -131,38 +104,32 @@ describe('WindowViewer', () => { }); }); }); - it('when view type changes', () => { - expect(wrapper.instance().canvasGroupings.groupings().length).toEqual(3); - wrapper.setProps({ - window: { - canvasIndex: 0, - view: 'book', - }, - }); - expect(wrapper.instance().canvasGroupings.groupings().length).toEqual(2); - }); describe('componentDidMount', () => { it('does not call fetchInfoResponse for a canvas that has no images', () => { const mockFnCanvas0 = jest.fn(); const mockFnCanvas2 = jest.fn(); - canvases = manifesto.create(emptyCanvasFixture).getSequences()[0].getCanvases(); + const canvases = manifesto.create(emptyCanvasFixture).getSequences()[0].getCanvases(); + + currentCanvases = [canvases[0]]; + mockWindow = { canvasIndex: 0, view: 'single', }; wrapper = createWrapper( { - canvases, + currentCanvases, fetchInfoResponse: mockFnCanvas0, window: mockWindow, }, ); expect(mockFnCanvas0).toHaveBeenCalledTimes(1); + currentCanvases = [canvases[2]]; wrapper = createWrapper( { - canvases, + currentCanvases, fetchInfoResponse: mockFnCanvas2, window: { canvasIndex: 2, view: 'single' }, }, @@ -171,9 +138,11 @@ describe('WindowViewer', () => { }); it('calls fetchAnnotation when otherContent is present', () => { const mockFnAnno = jest.fn(); - canvases = manifesto.create(otherContentFixture).getSequences()[0].getCanvases(); + const canvases = manifesto.create(otherContentFixture).getSequences()[0].getCanvases(); + currentCanvases = [canvases[0]]; + wrapper = createWrapper( - { canvases, fetchAnnotation: mockFnAnno }, + { currentCanvases, fetchAnnotation: mockFnAnno }, ); expect(mockFnAnno).toHaveBeenCalledTimes(1); }); @@ -182,16 +151,17 @@ describe('WindowViewer', () => { describe('componentDidUpdate', () => { it('does not call fetchInfoResponse for a canvas that has no images', () => { const mockFn = jest.fn(); - canvases = manifesto.create(emptyCanvasFixture).getSequences()[0].getCanvases(); + const canvases = manifesto.create(emptyCanvasFixture).getSequences()[0].getCanvases(); + currentCanvases = [canvases[2]]; mockWindow = { canvasIndex: 2, view: 'single', }; wrapper = createWrapper( - { canvases, fetchInfoResponse: mockFn, window: mockWindow }, + { currentCanvases, fetchInfoResponse: mockFn, window: mockWindow }, ); - wrapper.setProps({ window: { canvasIndex: 3, view: 'single' } }); + wrapper.setProps({ currentCanvases: [canvases[3]], window: { canvasIndex: 3, view: 'single' } }); expect(mockFn).toHaveBeenCalledTimes(0); }); diff --git a/__tests__/src/selectors/index.test.js b/__tests__/src/selectors/index.test.js index b814ddaeadecb3065bc890f7ac21b220ff6a9f17..cf556213dc32c9f745b3c989cdbb0df104cf0739 100644 --- a/__tests__/src/selectors/index.test.js +++ b/__tests__/src/selectors/index.test.js @@ -1,128 +1,65 @@ import { - getAllOrSelectedAnnotations, + getAllOrSelectedAnnotationsOnCanvases, getAnnotationResourcesByMotivation, - getIdAndContentOfResources, getLanguagesFromConfigWithCurrent, getSelectedAnnotationIds, - getSelectedTargetAnnotations, - getSelectedTargetsAnnotations, - getSelectedTargetAnnotationResources, } from '../../../src/state/selectors'; -import Annotation from '../../../src/lib/Annotation'; -import AnnotationResource from '../../../src/lib/AnnotationResource'; -describe('getSelectedTargetAnnotations', () => { - it('returns annotations for the given canvasId that have resources', () => { +describe('getAnnotationResourcesByMotivation', () => { + it('returns an array of annotation resources (filtered by the passed in array of motiviations)', () => { + const expected = [ + ['oa:commenting'], + ['sc:something-else', 'oa:commenting'], + ]; + const state = { annotations: { - abc123: { - annoId1: { '@id': 'annoId1', json: { resources: ['aResource'] } }, - annoId2: { '@id': 'annoId2' }, - annoId3: { '@id': 'annoId3', json: { resources: [] } }, + cid1: { + annoId1: { + id: 'annoId1', + json: { + resources: [ + { '@id': 'annoId1', motivation: 'oa:commenting' }, + { '@id': 'annoId2', motivation: 'oa:not-commenting' }, + { '@id': 'annoId3', motivation: ['sc:something-else', 'oa:commenting'] }, + ], + }, + }, }, }, - }; - - expect(getSelectedTargetAnnotations(state, 'abc123').length).toEqual(1); - }); - - it('returns an empty array if there are no annotations', () => { - const state = { annotations: { xyz321: {} } }; - const expected = []; - - expect(getSelectedTargetAnnotations({}, 'abc123')).toEqual(expected); - expect(getSelectedTargetAnnotations(state, 'abc123')).toEqual(expected); - }); -}); - -describe('getSelectedTargetsAnnotations', () => { - it('returns annotations for multiple canvasIds', () => { - const state = { - annotations: { - abc123: { - annoId1: { '@id': 'annoId1', json: { resources: ['aResource'] } }, - annoId2: { '@id': 'annoId2' }, - annoId3: { '@id': 'annoId3', json: { resources: [] } }, + manifests: { + mid: { + json: { + '@context': 'http://iiif.io/api/presentation/2/context.json', + '@id': + 'http://iiif.io/api/presentation/2.1/example/fixtures/19/manifest.json', + '@type': 'sc:Manifest', + sequences: [ + { + canvases: [ + { + '@id': 'cid1', + }, + ], + }, + ], + }, }, - def456: { - annoId4: { '@id': 'annoId4', json: { resources: ['helloWorld'] } }, + }, + windows: { + abc123: { + canvasIndex: 0, + manifestId: 'mid', }, }, }; - expect(getSelectedTargetsAnnotations(state, ['abc123', 'def456']).length).toEqual(2); - }); - - it('returns an empty array if there are no annotations', () => { - const state = { annotations: { xyz321: {} } }; - const expected = []; - - expect(getSelectedTargetsAnnotations({}, ['abc123'])).toEqual(expected); - expect(getSelectedTargetsAnnotations(state, ['abc123'])).toEqual(expected); - }); -}); - -describe('getAnnotationResourcesByMotivation', () => { - const annotations = [ - new Annotation({ resources: [{ motivation: 'oa:commenting' }] }), - new Annotation({ resources: [{ motivation: 'oa:not-commenting' }] }), - new Annotation({ resources: [{ motivation: ['sc:something-else', 'oa:commenting'] }] }), - ]; - - it('returns an array of annotation resources (filtered by the passed in array of motiviations)', () => { - const expected = [ - ['oa:commenting'], - ['sc:something-else', 'oa:commenting'], - ]; - expect( - getAnnotationResourcesByMotivation(annotations, ['something', 'oa:commenting']).map(r => r.motivations), + getAnnotationResourcesByMotivation(state, { motivations: ['something', 'oa:commenting'], windowId: 'abc123' }).map(r => r.motivations), ).toEqual(expected); }); }); -describe('getIdAndContentOfResources', () => { - it('returns an array if id/content objects from the annotation resources', () => { - const annotations = [ - new AnnotationResource({ '@id': 'theId', on: 'example.com', resource: { chars: 'The Content' } }), - ]; - const expected = [ - { - content: 'The Content', - id: 'theId', - targetId: 'example.com', - }, - ]; - - expect(getIdAndContentOfResources(annotations)).toEqual(expected); - }); - - it('provides an ID as a UUID if the annotation does not have one', () => { - const annotations = [ - new AnnotationResource({ resource: { chars: 'The Content' } }), - ]; - - expect(getIdAndContentOfResources(annotations)[0].id).toEqual( - expect.stringMatching(/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/), - ); - }); - - it('handles resource arrays', () => { - const annotations = [ - new AnnotationResource({ '@id': 'theId', on: 'example.com', resource: [{ chars: 'The' }, { chars: 'Content' }] }), - ]; - const expected = [ - { - content: 'The Content', - id: 'theId', - targetId: 'example.com', - }, - ]; - - expect(getIdAndContentOfResources(annotations)).toEqual(expected); - }); -}); - describe('getLanguagesFromConfigWithCurrent', () => { it('returns an array of objects with locale, label, and current properties', () => { const state = { @@ -154,8 +91,29 @@ describe('getLanguagesFromConfigWithCurrent', () => { it('getSelectedAnnotationIds returns an array of selected annotation IDs from state', () => { const state = { + manifests: { + mid: { + json: { + '@context': 'http://iiif.io/api/presentation/2/context.json', + '@id': + 'http://iiif.io/api/presentation/2.1/example/fixtures/19/manifest.json', + '@type': 'sc:Manifest', + sequences: [ + { + canvases: [ + { + '@id': 'tid1', + }, + ], + }, + ], + }, + }, + }, windows: { wid: { + canvasIndex: 0, + manifestId: 'mid', selectedAnnotations: { tid1: ['aid1', 'aid2'], tid2: ['aid3'], @@ -164,33 +122,12 @@ it('getSelectedAnnotationIds returns an array of selected annotation IDs from st }, }; - expect(getSelectedAnnotationIds(state, 'wid', ['tid2'])).toEqual( - ['aid3'], - ); - expect(getSelectedAnnotationIds(state, 'wid', ['tid1', 'tid2'])).toEqual( - ['aid1', 'aid2', 'aid3'], + expect(getSelectedAnnotationIds(state, { windowId: 'wid' })).toEqual( + ['aid1', 'aid2'], ); }); -it('getSelectedTargetAnnotationResources filters the annotation resources by the annotationIds passed in', () => { - const state = { - annotations: { - cid1: { - annoId1: { id: 'annoId1', json: { resources: [{ '@id': 'annoId1' }, { '@id': 'annoId2' }] } }, - }, - }, - }; - - expect( - getSelectedTargetAnnotationResources(state, ['cid1'], ['annoId1'])[0].resources.length, - ).toBe(1); - - expect( - getSelectedTargetAnnotationResources(state, ['cid1'], ['annoId1', 'annoId2'])[0].resources.length, - ).toBe(2); -}); - -describe('getAllOrSelectedAnnotations', () => { +describe('getAllOrSelectedAnnotationsOnCanvases', () => { it('returns all annotations if the given window is set to display all', () => { const state = { annotations: { @@ -198,13 +135,32 @@ describe('getAllOrSelectedAnnotations', () => { annoId1: { id: 'annoId1', json: { resources: [{ '@id': 'annoId1' }, { '@id': 'annoId2' }] } }, }, }, + manifests: { + mid: { + json: { + '@context': 'http://iiif.io/api/presentation/2/context.json', + '@id': + 'http://iiif.io/api/presentation/2.1/example/fixtures/19/manifest.json', + '@type': 'sc:Manifest', + sequences: [ + { + canvases: [ + { + '@id': 'cid1', + }, + ], + }, + ], + }, + }, + }, windows: { - abc123: { displayAllAnnotations: true }, + abc123: { canvasIndex: 0, displayAllAnnotations: true, manifestId: 'mid' }, }, }; expect( - getAllOrSelectedAnnotations(state, 'abc123', ['cid1'], ['annoId1'])[0].resources.length, + getAllOrSelectedAnnotationsOnCanvases(state, { windowId: 'abc123' })[0].resources.length, ).toBe(2); }); @@ -215,13 +171,73 @@ describe('getAllOrSelectedAnnotations', () => { annoId1: { id: 'annoId1', json: { resources: [{ '@id': 'annoId1' }, { '@id': 'annoId2' }] } }, }, }, + manifests: { + mid: { + json: { + '@context': 'http://iiif.io/api/presentation/2/context.json', + '@id': + 'http://iiif.io/api/presentation/2.1/example/fixtures/19/manifest.json', + '@type': 'sc:Manifest', + sequences: [ + { + canvases: [ + { + '@id': 'cid1', + }, + ], + }, + ], + }, + }, + }, + windows: { + abc123: { + canvasIndex: 0, + displayAllAnnotations: false, + manifestId: 'mid', + selectedAnnotations: { cid1: ['annoId1'] }, + }, + }, + }; + + expect( + getAllOrSelectedAnnotationsOnCanvases(state, { windowId: 'abc123' })[0].resources.length, + ).toBe(1); + }); + + it('filters the annotation resources by the selected annotations for the window', () => { + const state = { + annotations: { + cid1: { + annoId1: { id: 'annoId1', json: { resources: [{ '@id': 'annoId2' }, { '@id': 'annoId3' }] } }, + }, + }, + manifests: { + mid: { + json: { + '@context': 'http://iiif.io/api/presentation/2/context.json', + '@id': + 'http://iiif.io/api/presentation/2.1/example/fixtures/19/manifest.json', + '@type': 'sc:Manifest', + sequences: [ + { + canvases: [ + { + '@id': 'cid1', + }, + ], + }, + ], + }, + }, + }, windows: { - abc123: { displayAllAnnotations: false }, + abc123: { canvasIndex: 0, manifestId: 'mid', selectedAnnotations: { cid1: ['annoId2'] } }, }, }; expect( - getAllOrSelectedAnnotations(state, 'abc123', ['cid1'], ['annoId1'])[0].resources.length, + getAllOrSelectedAnnotationsOnCanvases(state, { windowId: 'abc123' })[0].resources.length, ).toBe(1); }); }); diff --git a/src/components/OpenSeadragonViewer.js b/src/components/OpenSeadragonViewer.js index 4c1f9533088aa6d75e18b69215982d846135eb9c..2fa636830ef3cb6f297fb21d0e29ce32c74e9f0a 100644 --- a/src/components/OpenSeadragonViewer.js +++ b/src/components/OpenSeadragonViewer.js @@ -132,11 +132,11 @@ export class OpenSeadragonViewer extends Component { * annotationsToContext - converts anontations to a canvas context */ annotationsToContext(annotations) { - const { currentCanvases } = this.props; + const { canvasWorld } = this.props; const context = this.osdCanvasOverlay.context2d; annotations.forEach((annotation) => { annotation.resources.forEach((resource) => { - const offset = new CanvasWorld(currentCanvases).offsetByCanvas(resource.targetId); + const offset = canvasWorld.offsetByCanvas(resource.targetId); const fragment = resource.fragmentSelector; fragment[0] += offset.x; context.strokeStyle = 'yellow'; @@ -149,7 +149,7 @@ export class OpenSeadragonViewer extends Component { /** */ addTileSource(tileSource, i = 0) { - const { currentCanvases } = this.props; + const { canvasWorld } = this.props; return new Promise((resolve, reject) => { if (!this.viewer) { return; @@ -157,7 +157,7 @@ export class OpenSeadragonViewer extends Component { this.viewer.addTiledImage({ error: event => reject(event), fitBounds: new OpenSeadragon.Rect( - ...new CanvasWorld(currentCanvases).canvasToWorldCoordinates(i), + ...canvasWorld.canvasToWorldCoordinates(i), ), success: event => resolve(event), tileSource, @@ -197,8 +197,8 @@ export class OpenSeadragonViewer extends Component { * zoomToWorld - zooms the viewer to the extent of the canvas world */ zoomToWorld(immediately = true) { - const { currentCanvases } = this.props; - this.fitBounds(...new CanvasWorld(currentCanvases).worldBounds(), immediately); + const { canvasWorld } = this.props; + this.fitBounds(...canvasWorld.worldBounds(), immediately); } /** @@ -262,7 +262,6 @@ OpenSeadragonViewer.defaultProps = { annotations: [], children: null, classes: {}, - currentCanvases: [], label: null, tileSources: [], viewer: null, @@ -271,9 +270,9 @@ OpenSeadragonViewer.defaultProps = { OpenSeadragonViewer.propTypes = { annotations: PropTypes.arrayOf(PropTypes.object), + canvasWorld: PropTypes.instanceOf(CanvasWorld).isRequired, children: PropTypes.element, classes: PropTypes.object, // eslint-disable-line react/forbid-prop-types - currentCanvases: PropTypes.arrayOf(PropTypes.object), label: PropTypes.string, t: PropTypes.func.isRequired, tileSources: PropTypes.arrayOf(PropTypes.object), diff --git a/src/components/WindowCanvasNavigationControls.js b/src/components/WindowCanvasNavigationControls.js index f8315c7070d58e412c5cfddca70c6623ad48c5a9..db6ddaad9cb6fd1562dc5caa4da00ac99376846b 100644 --- a/src/components/WindowCanvasNavigationControls.js +++ b/src/components/WindowCanvasNavigationControls.js @@ -12,7 +12,7 @@ export class WindowCanvasNavigationControls extends Component { /** */ render() { const { - canvases, visible, window, zoomToWorld, + visible, window, zoomToWorld, } = this.props; if (!visible) return (<></>); @@ -20,7 +20,7 @@ export class WindowCanvasNavigationControls extends Component { return ( <div className={ns('canvas-nav')}> <ZoomControls windowId={window.id} zoomToWorld={zoomToWorld} /> - <ViewerNavigation window={window} canvases={canvases} /> + <ViewerNavigation window={window} /> <ViewerInfo windowId={window.id} /> </div> ); @@ -29,7 +29,6 @@ export class WindowCanvasNavigationControls extends Component { WindowCanvasNavigationControls.propTypes = { - canvases: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types visible: PropTypes.bool, window: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types zoomToWorld: PropTypes.func.isRequired, diff --git a/src/components/WindowViewer.js b/src/components/WindowViewer.js index 87fabc8dc80882e46edc6007e30d8d98a4c8e7bd..65337429efa7c7d97c8c9eb0b4bb76826abdb08b 100644 --- a/src/components/WindowViewer.js +++ b/src/components/WindowViewer.js @@ -3,33 +3,21 @@ import PropTypes from 'prop-types'; import OSDViewer from '../containers/OpenSeadragonViewer'; import WindowCanvasNavigationControls from '../containers/WindowCanvasNavigationControls'; import ManifestoCanvas from '../lib/ManifestoCanvas'; -import CanvasGroupings from '../lib/CanvasGroupings'; /** * Represents a WindowViewer in the mirador workspace. Responsible for mounting * OSD and Navigation */ export class WindowViewer extends Component { - /** - * @param {Object} props - */ - constructor(props) { - super(props); - - const { canvases, window } = this.props; - this.canvases = canvases; - this.canvasGroupings = new CanvasGroupings(this.canvases, window.view); - } - /** * componentDidMount - React lifecycle method * Request the initial canvas on mount */ componentDidMount() { - const { fetchInfoResponse, fetchAnnotation } = this.props; + const { currentCanvases, fetchInfoResponse, fetchAnnotation } = this.props; if (!this.infoResponseIsInStore()) { - this.currentCanvases().forEach((canvas) => { + currentCanvases.forEach((canvas) => { const manifestoCanvas = new ManifestoCanvas(canvas); const { imageInformationUri } = manifestoCanvas; if (imageInformationUri) { @@ -47,11 +35,14 @@ export class WindowViewer extends Component { * Request a new canvas if it is needed */ componentDidUpdate(prevProps) { - const { window, fetchInfoResponse, fetchAnnotation } = this.props; + const { + currentCanvases, window, fetchInfoResponse, fetchAnnotation, + } = this.props; + if (prevProps.window.view !== window.view || (prevProps.window.canvasIndex !== window.canvasIndex && !this.infoResponseIsInStore()) ) { - this.currentCanvases().forEach((canvas) => { + currentCanvases.forEach((canvas) => { const manifestoCanvas = new ManifestoCanvas(canvas); const { imageInformationUri } = manifestoCanvas; if (imageInformationUri) { @@ -62,10 +53,6 @@ export class WindowViewer extends Component { }); }); } - // If the view changes, create a new instance - if (prevProps.window.view !== window.view) { - this.canvasGroupings = new CanvasGroupings(this.canvases, window.view); - } } /** @@ -74,29 +61,22 @@ export class WindowViewer extends Component { * @return [Boolean] */ infoResponseIsInStore() { + const { currentCanvases } = this.props; + const responses = this.currentInfoResponses(); - if (responses.length === this.currentCanvases().length) { + if (responses.length === currentCanvases.length) { return true; } return false; } - /** - * Uses CanvasGroupings to figure out how many and what canvases to present to - * a user based off of the view, number of canvases, and canvasIndex. - */ - currentCanvases() { - const { window } = this.props; - return this.canvasGroupings.getCanvases(window.canvasIndex); - } - /** * currentInfoResponses - Selects infoResponses that are relevent to existing * canvases to be displayed. */ currentInfoResponses() { - const { infoResponses } = this.props; - const currentCanvases = this.currentCanvases(); + const { currentCanvases, infoResponses } = this.props; + return currentCanvases.map(canvas => ( infoResponses[new ManifestoCanvas(canvas).imageInformationUri] )).filter(infoResponse => (infoResponse !== undefined @@ -108,10 +88,12 @@ export class WindowViewer extends Component { * Return an image information response from the store for the correct image */ tileInfoFetchedFromStore() { + const { currentCanvases } = this.props; + const responses = this.currentInfoResponses() .map(infoResponse => infoResponse.json); // Only return actual tileSources when all current canvases have completed. - if (responses.length === this.currentCanvases().length) { + if (responses.length === currentCanvases.length) { return responses; } return []; @@ -126,10 +108,9 @@ export class WindowViewer extends Component { <> <OSDViewer tileSources={this.tileInfoFetchedFromStore()} - currentCanvases={this.currentCanvases()} windowId={window.id} > - <WindowCanvasNavigationControls windowId={window.id} canvases={this.canvases} /> + <WindowCanvasNavigationControls windowId={window.id} /> </OSDViewer> </> ); @@ -137,7 +118,7 @@ export class WindowViewer extends Component { } WindowViewer.propTypes = { - canvases: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types + currentCanvases: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types fetchAnnotation: PropTypes.func.isRequired, fetchInfoResponse: PropTypes.func.isRequired, infoResponses: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types diff --git a/src/containers/AnnotationSettings.js b/src/containers/AnnotationSettings.js index 5fbec40b2e8c34e514c2e1aa4447d81c32f0fb1a..b56ab73ac4eb3e3f76a33766457886098c7f6913 100644 --- a/src/containers/AnnotationSettings.js +++ b/src/containers/AnnotationSettings.js @@ -5,8 +5,6 @@ import * as actions from '../state/actions'; import { withPlugins } from '../extend'; import { getAnnotationResourcesByMotivation, - getSelectedTargetAnnotations, - getSelectedCanvas, } from '../state/selectors'; import { AnnotationSettings } from '../components/AnnotationSettings'; @@ -15,10 +13,7 @@ import { AnnotationSettings } from '../components/AnnotationSettings'; */ const mapStateToProps = (state, { windowId }) => ({ displayAll: state.windows[windowId].displayAllAnnotations, - displayAllDisabled: getAnnotationResourcesByMotivation( - getSelectedTargetAnnotations(state, (getSelectedCanvas(state, { windowId }) || {}).id), - ['oa:commenting', 'sc:painting'], - ).length < 2, + displayAllDisabled: getAnnotationResourcesByMotivation(state, { motivations: ['oa:commenting', 'sc:painting'], windowId }).length < 2, }); /** diff --git a/src/containers/OpenSeadragonViewer.js b/src/containers/OpenSeadragonViewer.js index 392f9d6e64aa0aabdca3c3ed2ebe6fab1fabc799..21ac19e3dedd22008a30fe8cb0120dfa40742eb9 100644 --- a/src/containers/OpenSeadragonViewer.js +++ b/src/containers/OpenSeadragonViewer.js @@ -6,10 +6,11 @@ import { fade } from '@material-ui/core/styles/colorManipulator'; import { withPlugins } from '../extend'; import { OpenSeadragonViewer } from '../components/OpenSeadragonViewer'; import * as actions from '../state/actions'; +import CanvasWorld from '../lib/CanvasWorld'; import { - getAllOrSelectedAnnotations, + getAllOrSelectedAnnotationsOnCanvases, getCanvasLabel, - getSelectedAnnotationIds, + getSelectedCanvases, } from '../state/selectors'; /** @@ -17,23 +18,11 @@ import { * @memberof Window * @private */ -const mapStateToProps = ({ - viewers, windows, manifests, annotations, -}, { windowId, currentCanvases }) => ({ - annotations: getAllOrSelectedAnnotations( - { annotations, windows }, - windowId, - currentCanvases.map(c => c.id), - getSelectedAnnotationIds({ windows }, windowId, currentCanvases.map(c => c.id)), - ), - label: getCanvasLabel({ - manifests, - windows, - }, { - canvasIndex: 'selected', - windowId, - }), - viewer: viewers[windowId], +const mapStateToProps = (state, { windowId }) => ({ + annotations: getAllOrSelectedAnnotationsOnCanvases(state, { windowId }), + canvasWorld: new CanvasWorld(getSelectedCanvases(state, { windowId })), + label: getCanvasLabel(state, { canvasIndex: 'selected', windowId }), + viewer: state.viewers[windowId], }); /** diff --git a/src/containers/ViewerNavigation.js b/src/containers/ViewerNavigation.js index d1edbed061c9122b2d08c78cebf871e955bd42ee..e5e804d7b0aa9163a657bd7f5a2f2af663cc0c76 100644 --- a/src/containers/ViewerNavigation.js +++ b/src/containers/ViewerNavigation.js @@ -3,8 +3,14 @@ import { connect } from 'react-redux'; import { withTranslation } from 'react-i18next'; import { withPlugins } from '../extend'; import * as actions from '../state/actions'; +import { getManifestCanvases } from '../state/selectors'; import { ViewerNavigation } from '../components/ViewerNavigation'; +/** */ +const mapStateToProps = (state, { window }) => ({ + canvases: getManifestCanvases(state, { windowId: window.id }), +}); + /** * mapDispatchToProps - used to hook up connect to action creators * @memberof ManifestForm @@ -16,7 +22,7 @@ const mapDispatchToProps = { const enhance = compose( withTranslation(), - connect(null, mapDispatchToProps), + connect(mapStateToProps, mapDispatchToProps), withPlugins('ViewerNavigation'), // further HOC go here ); diff --git a/src/containers/WindowSideBarAnnotationsPanel.js b/src/containers/WindowSideBarAnnotationsPanel.js index 12b90c12df81541b918deb5f504cb826a0ae2e6f..53cdb2e4cac0ee9631c5a39aceef12ce462e3b8a 100644 --- a/src/containers/WindowSideBarAnnotationsPanel.js +++ b/src/containers/WindowSideBarAnnotationsPanel.js @@ -5,14 +5,23 @@ import { withStyles } from '@material-ui/core/styles'; import { withPlugins } from '../extend'; import * as actions from '../state/actions'; import { - getIdAndContentOfResources, getSelectedAnnotationIds, - getSelectedCanvases, - getSelectedTargetsAnnotations, getAnnotationResourcesByMotivation, } from '../state/selectors'; import { WindowSideBarAnnotationsPanel } from '../components/WindowSideBarAnnotationsPanel'; +/** + * @param {Array} resources + * @return {Array} [{ id: 'abc123', content: 'Annotation Content' }, ...] + */ +function getIdAndContentOfResources(resources) { + return resources.map((resource, i) => ({ + content: resource.chars, + id: resource.id, + targetId: resource.targetId, + })); +} + /** * mapStateToProps - to hook up connect * @memberof WindowSideBarAnnotationsPanel @@ -20,17 +29,9 @@ import { WindowSideBarAnnotationsPanel } from '../components/WindowSideBarAnnota */ const mapStateToProps = (state, { windowId }) => ({ annotations: getIdAndContentOfResources( - getAnnotationResourcesByMotivation( - getSelectedTargetsAnnotations( - state, - getSelectedCanvases(state, { windowId }).map(canvas => canvas.id), - ), - ['oa:commenting', 'sc:painting'], - ), - ), - selectedAnnotationIds: getSelectedAnnotationIds( - state, windowId, getSelectedCanvases(state, { windowId }).map(canvas => canvas.id), + getAnnotationResourcesByMotivation(state, { motivations: ['oa:commenting', 'sc:painting'], windowId }), ), + selectedAnnotationIds: getSelectedAnnotationIds(state, { windowId }), }); /** diff --git a/src/containers/WindowSideBarButtons.js b/src/containers/WindowSideBarButtons.js index 4e0e8d5b2b74122429fa9c46be31295f89edab5e..f4fc77482ea64b600e61764ebf104c86f3e86f06 100644 --- a/src/containers/WindowSideBarButtons.js +++ b/src/containers/WindowSideBarButtons.js @@ -7,8 +7,6 @@ import { withPlugins } from '../extend'; import * as actions from '../state/actions'; import { getCompanionWindowForPosition, - getSelectedCanvas, - getSelectedTargetAnnotations, getAnnotationResourcesByMotivation, } from '../state/selectors'; import { WindowSideBarButtons } from '../components/WindowSideBarButtons'; @@ -32,14 +30,8 @@ const mapDispatchToProps = (dispatch, { windowId }) => ({ * @private */ const mapStateToProps = (state, { windowId }) => ({ - hasAnnotations: getAnnotationResourcesByMotivation( - getSelectedTargetAnnotations(state, (getSelectedCanvas(state, { windowId }) || {}).id), - ['oa:commenting', 'sc:painting'], - ).length > 0, - sideBarPanel: (getCompanionWindowForPosition(state, { - position: 'left', - windowId, - }) || {}).content, + hasAnnotations: getAnnotationResourcesByMotivation(state, { motivations: ['oa:commenting', 'sc:painting'], windowId }).length > 0, + sideBarPanel: (getCompanionWindowForPosition(state, { position: 'left', windowId }) || {}).content, }); /** */ diff --git a/src/containers/WindowViewer.js b/src/containers/WindowViewer.js index 0aa4e8a7cc6cd761465f1be2810bebb7f6db1e71..1f109343d74a4d9a0dd4d78b91f3a1d3723e604e 100644 --- a/src/containers/WindowViewer.js +++ b/src/containers/WindowViewer.js @@ -3,7 +3,7 @@ import { connect } from 'react-redux'; import { withPlugins } from '../extend'; import * as actions from '../state/actions'; import { WindowViewer } from '../components/WindowViewer'; -import { getManifestCanvases } from '../state/selectors'; +import { getSelectedCanvases } from '../state/selectors'; /** * mapStateToProps - to hook up connect @@ -12,7 +12,7 @@ import { getManifestCanvases } from '../state/selectors'; */ const mapStateToProps = (state, { window }) => ( { - canvases: getManifestCanvases(state, { windowId: window.id }), + currentCanvases: getSelectedCanvases(state, { windowId: window.id }), infoResponses: state.infoResponses, } ); diff --git a/src/state/selectors/index.js b/src/state/selectors/index.js index cc07574c6a8bf1a10fc616351274e308b81d8c29..9b2903a8646f8fa3490c7c0961b12e7e067b35eb 100644 --- a/src/state/selectors/index.js +++ b/src/state/selectors/index.js @@ -1,42 +1,37 @@ +import { createSelector } from 'reselect'; import filter from 'lodash/filter'; import flatten from 'lodash/flatten'; import Annotation from '../../lib/Annotation'; +import { getSelectedCanvases } from './canvases'; export * from './canvases'; export * from './manifests'; export * from './windows'; -/** -* Return annotations for an array of targets -* @param {object} state -* @param {Array} targets -* @return {Array} -*/ -export function getSelectedTargetsAnnotations(state, targets) { - const annotations = state.annotations - && targets.map(target => getSelectedTargetAnnotations(state, target)); - if (!annotations) return []; +const getAnnotationsOnSelectedCanvases = createSelector( + [ + getSelectedCanvases, + state => state.annotations, + ], + (canvases, annotations) => { + if (!annotations || !canvases) return []; + return flatten( + canvases.map(c => c.id).map( + targetId => annotations[targetId] && Object.values(annotations[targetId]), + ), + ); + }, +); - return flatten(annotations); -} - -/** -* Return a single target's annotations -* @param {object} state -* @param {String} target -* @return {Array} -*/ -export function getSelectedTargetAnnotations(state, target) { - const annotations = state.annotations && state.annotations[target]; - - if (!annotations) return []; - - return filter( - Object.keys(annotations).map(id => new Annotation(annotations[id].json, target)), - annotation => annotation - && annotation.present(), - ); -} +const getPresentAnnotationsOnSelectedCanvases = createSelector( + [ + getAnnotationsOnSelectedCanvases, + ], + annotations => filter( + Object.values(annotations).map(annotation => annotation && new Annotation(annotation.json)), + annotation => annotation && annotation.present(), + ), +); /** * Return an array of annotation resources filtered by the given motivation @@ -44,25 +39,18 @@ export function getSelectedTargetAnnotations(state, target) { * @param {Array} motivations * @return {Array} */ -export function getAnnotationResourcesByMotivation(annotations, motivations) { - const resources = flatten(annotations.map(annotation => annotation.resources)); - - return filter(resources, resource => resource.motivations.some( - motivation => motivations.includes(motivation), - )); -} - -/** - * @param {Array} resources - * @return {Array} [{ id: 'abc123', content: 'Annotation Content' }, ...] - */ -export function getIdAndContentOfResources(resources) { - return resources.map((resource, i) => ({ - content: resource.chars, - id: resource.id, - targetId: resource.targetId, - })); -} +export const getAnnotationResourcesByMotivation = createSelector( + [ + getPresentAnnotationsOnSelectedCanvases, + (state, { motivations }) => motivations, + ], + (annotations, motivations) => filter( + flatten(annotations.map(annotation => annotation.resources)), + resource => resource.motivations.some( + motivation => motivations.includes(motivation), + ), + ), +); /** * Return languages from config (in state) and indicate which is currently set @@ -86,39 +74,32 @@ export function getLanguagesFromConfigWithCurrent(state) { * @param {Array} targetIds * @return {Array} */ -export function getSelectedAnnotationIds(state, windowId, targetIds) { - return flatten(targetIds.map(targetId => state.windows[windowId].selectedAnnotations - && state.windows[windowId].selectedAnnotations[targetId])); -} +export const getSelectedAnnotationIds = createSelector( + [ + (state, { windowId }) => state.windows[windowId].selectedAnnotations, + getSelectedCanvases, + ], + (selectedAnnotations, canvases) => ( + flatten( + canvases.map(c => c.id).map(targetId => selectedAnnotations && selectedAnnotations[targetId]), + ) + ), +); -/** -* Return the current canvas' (selected in a window) selected annotations -* @param {object} state -* @param {Array} targetIds -* @param {Array} annotationIds -* @return {Array} -*/ -export function getSelectedTargetAnnotationResources(state, targetIds, annotationIds) { - return getSelectedTargetsAnnotations(state, targetIds) - .map(annotation => ({ +export const getAllOrSelectedAnnotationsOnCanvases = createSelector( + [ + getPresentAnnotationsOnSelectedCanvases, + getSelectedAnnotationIds, + (state, { windowId }) => state.windows[windowId].displayAllAnnotations, + ], + (canvasAnnotations, selectedAnnotationIds, displayAllAnnotations) => { + if (displayAllAnnotations) return canvasAnnotations; + + return canvasAnnotations.map(annotation => ({ id: (annotation['@id'] || annotation.id), - resources: annotation.resources.filter(r => annotationIds && annotationIds.includes(r.id)), + resources: annotation.resources.filter( + r => selectedAnnotationIds && selectedAnnotationIds.includes(r.id), + ), })); -} - -/** -* Return all of the given canvases annotations if the window -* is set to display all, otherwise only return selected -* @param {object} state -* @param {String} windowId -* @param {Array} targetIds -* @param {Array} annotationIds -* @return {Array} -*/ -export function getAllOrSelectedAnnotations(state, windowId, targetIds, annotationIds) { - if (state.windows[windowId].displayAllAnnotations) { - return getSelectedTargetsAnnotations(state, targetIds); - } - - return getSelectedTargetAnnotationResources(state, targetIds, annotationIds); -} + }, +);