Skip to content
Snippets Groups Projects
Commit 035e1354 authored by Chris Beer's avatar Chris Beer
Browse files

Add OSD-internal state information to the workspace state; fixes #1644

parent 5c5f36d2
No related branches found
No related tags found
No related merge requests found
...@@ -33,4 +33,19 @@ describe('canvas actions', () => { ...@@ -33,4 +33,19 @@ describe('canvas actions', () => {
expect(actions.setCanvas(id, 100)).toEqual(expectedAction); expect(actions.setCanvas(id, 100)).toEqual(expectedAction);
}); });
}); });
describe('updateViewport', () => {
it('sets viewer state', () => {
const id = 'abc123';
const expectedAction = {
type: ActionTypes.UPDATE_VIEWPORT,
windowId: id,
payload: {
x: 1,
y: 0,
zoom: 0.5,
},
};
expect(actions.updateViewport(id, { x: 1, y: 0, zoom: 0.5 })).toEqual(expectedAction);
});
});
}); });
import React from 'react'; import React from 'react';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import OpenSeadragon from 'openseadragon';
import OpenSeadragonViewer from '../../../src/components/OpenSeadragonViewer'; import OpenSeadragonViewer from '../../../src/components/OpenSeadragonViewer';
jest.mock('openseadragon');
describe('OpenSeadragonViewer', () => { describe('OpenSeadragonViewer', () => {
let wrapper; let wrapper;
let updateViewport;
beforeEach(() => { beforeEach(() => {
OpenSeadragon.mockClear();
updateViewport = jest.fn();
wrapper = shallow( wrapper = shallow(
<OpenSeadragonViewer <OpenSeadragonViewer
tileSources={[{ '@id': 'http://foo' }]} tileSources={[{ '@id': 'http://foo' }]}
window={{ id: 'base' }} window={{ id: 'base' }}
config={{}} config={{}}
updateViewport={updateViewport}
> >
<div className="foo" /> <div className="foo" />
</OpenSeadragonViewer>, </OpenSeadragonViewer>,
...@@ -31,9 +40,6 @@ describe('OpenSeadragonViewer', () => { ...@@ -31,9 +40,6 @@ describe('OpenSeadragonViewer', () => {
}); });
describe('addTileSource', () => { describe('addTileSource', () => {
it('calls addTiledImage asynchronously on the OSD viewer', async () => { it('calls addTiledImage asynchronously on the OSD viewer', async () => {
wrapper.instance().viewer = {
addTiledImage: jest.fn().mockResolvedValue('event'),
};
wrapper.instance().addTileSource({}).then((event) => { wrapper.instance().addTileSource({}).then((event) => {
expect(event).toBe('event'); expect(event).toBe('event');
}); });
...@@ -59,4 +65,87 @@ describe('OpenSeadragonViewer', () => { ...@@ -59,4 +65,87 @@ describe('OpenSeadragonViewer', () => {
).toHaveBeenCalled(); ).toHaveBeenCalled();
}); });
}); });
describe('componentDidMount', () => {
let panTo;
let zoomTo;
let addHandler;
beforeEach(() => {
panTo = jest.fn();
zoomTo = jest.fn();
addHandler = jest.fn();
wrapper = shallow(
<OpenSeadragonViewer
tileSources={[{ '@id': 'http://foo' }]}
window={{ id: 'base', viewer: { x: 1, y: 0, zoom: 0.5 } }}
config={{}}
updateViewport={updateViewport}
>
<div className="foo" />
</OpenSeadragonViewer>,
);
wrapper.instance().ref = { current: true };
OpenSeadragon.mockImplementation(() => ({
viewport: { panTo, zoomTo },
addHandler,
addTiledImage: jest.fn().mockResolvedValue('event'),
}));
});
it('calls the OSD viewport panTo and zoomTo with the component state', () => {
wrapper.instance().componentDidMount();
expect(addHandler).toHaveBeenCalledWith('viewport-change', expect.anything());
expect(panTo).toHaveBeenCalledWith(
{ x: 1, y: 0, zoom: 0.5 }, false,
);
expect(zoomTo).toHaveBeenCalledWith(
0.5, { x: 1, y: 0, zoom: 0.5 }, false,
);
});
});
describe('componentDidUpdate', () => {
it('calls the OSD viewport panTo and zoomTo with the component state', () => {
const panTo = jest.fn();
const zoomTo = jest.fn();
wrapper.instance().viewer = {
viewport: { panTo, zoomTo },
};
wrapper.setProps({ window: { id: 'base', viewer: { x: 0.5, y: 0.5, zoom: 0.1 } } });
wrapper.setProps({ window: { id: 'base', viewer: { x: 1, y: 0, zoom: 0.5 } } });
expect(panTo).toHaveBeenCalledWith(
{ x: 1, y: 0, zoom: 0.5 }, false,
);
expect(zoomTo).toHaveBeenCalledWith(
0.5, { x: 1, y: 0, zoom: 0.5 }, false,
);
});
});
describe('onViewportChange', () => {
it('translates the OSD viewport data into an update to the component state', () => {
wrapper.instance().onViewportChange({
eventSource: {
viewport: {
centerSpringX: { target: { value: 1 } },
centerSpringY: { target: { value: 0 } },
zoomSpring: { target: { value: 0.5 } },
},
},
});
expect(updateViewport).toHaveBeenCalledWith(
'base',
{ x: 1, y: 0, zoom: 0.5 },
);
});
});
}); });
...@@ -138,4 +138,27 @@ describe('windows reducer', () => { ...@@ -138,4 +138,27 @@ describe('windows reducer', () => {
}, },
}); });
}); });
it('should handle UPDATE_VIEWPORT', () => {
expect(reducer({
abc123: {
id: 'abc123',
},
def456: {
id: 'def456',
},
}, {
type: ActionTypes.UPDATE_VIEWPORT,
windowId: 'abc123',
payload: { x: 0, y: 1, zoom: 0.5 },
})).toEqual({
abc123: {
id: 'abc123',
viewer: { x: 0, y: 1, zoom: 0.5 },
},
def456: {
id: 'def456',
},
});
});
}); });
...@@ -16,13 +16,14 @@ class OpenSeadragonViewer extends Component { ...@@ -16,13 +16,14 @@ class OpenSeadragonViewer extends Component {
this.viewer = null; this.viewer = null;
this.ref = React.createRef(); this.ref = React.createRef();
this.onViewportChange = this.onViewportChange.bind(this);
} }
/** /**
* React lifecycle event * React lifecycle event
*/ */
componentDidMount() { componentDidMount() {
const { tileSources } = this.props; const { tileSources, window } = this.props;
if (!this.ref.current) { if (!this.ref.current) {
return; return;
} }
...@@ -33,14 +34,22 @@ class OpenSeadragonViewer extends Component { ...@@ -33,14 +34,22 @@ class OpenSeadragonViewer extends Component {
alwaysBlend: false, alwaysBlend: false,
showNavigationControl: false, showNavigationControl: false,
}); });
this.viewer.addHandler('viewport-change', this.onViewportChange);
if (window.viewer) {
this.viewer.viewport.panTo(window.viewer, false);
this.viewer.viewport.zoomTo(window.viewer.zoom, window.viewer, false);
}
tileSources.forEach(tileSource => this.addTileSource(tileSource)); tileSources.forEach(tileSource => this.addTileSource(tileSource));
} }
/** /**
* When the tileSources change, make sure to close the OSD viewer. * When the tileSources change, make sure to close the OSD viewer.
* When the viewport state changes, pan or zoom the OSD viewer as appropriate
*/ */
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
const { tileSources } = this.props; const { tileSources, window } = this.props;
if (!this.tileSourcesMatch(prevProps.tileSources)) { if (!this.tileSourcesMatch(prevProps.tileSources)) {
this.viewer.close(); this.viewer.close();
Promise.all( Promise.all(
...@@ -52,6 +61,15 @@ class OpenSeadragonViewer extends Component { ...@@ -52,6 +61,15 @@ class OpenSeadragonViewer extends Component {
this.fitBounds(0, 0, tileSources[0].width, tileSources[0].height); this.fitBounds(0, 0, tileSources[0].width, tileSources[0].height);
} }
}); });
} else if (window.viewer && prevProps.window.viewer) {
if (window.viewer.x !== prevProps.window.viewer.x
|| window.viewer.y !== prevProps.window.viewer.y) {
this.viewer.viewport.panTo(window.viewer, false);
}
if (window.viewer.zoom !== prevProps.window.viewer.zoom) {
this.viewer.viewport.zoomTo(window.viewer.zoom, window.viewer, false);
}
} }
} }
...@@ -61,6 +79,21 @@ class OpenSeadragonViewer extends Component { ...@@ -61,6 +79,21 @@ class OpenSeadragonViewer extends Component {
this.viewer.removeAllHandlers(); this.viewer.removeAllHandlers();
} }
/**
* Forward OSD state to redux
*/
onViewportChange(event) {
const { updateViewport, window } = this.props;
const { viewport } = event.eventSource;
updateViewport(window.id, {
x: viewport.centerSpringX.target.value,
y: viewport.centerSpringY.target.value,
zoom: viewport.zoomSpring.target.value,
});
}
/** /**
*/ */
addTileSource(tileSource) { addTileSource(tileSource) {
...@@ -132,6 +165,7 @@ OpenSeadragonViewer.propTypes = { ...@@ -132,6 +165,7 @@ OpenSeadragonViewer.propTypes = {
children: PropTypes.element, children: PropTypes.element,
tileSources: PropTypes.arrayOf(PropTypes.object), tileSources: PropTypes.arrayOf(PropTypes.object),
window: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types window: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
updateViewport: PropTypes.func.isRequired,
}; };
export default OpenSeadragonViewer; export default OpenSeadragonViewer;
import { compose } from 'redux';
import { connect } from 'react-redux';
import miradorWithPlugins from '../lib/miradorWithPlugins'; import miradorWithPlugins from '../lib/miradorWithPlugins';
import OpenSeadragonViewer from '../components/OpenSeadragonViewer'; import OpenSeadragonViewer from '../components/OpenSeadragonViewer';
import * as actions from '../state/actions';
export default miradorWithPlugins(OpenSeadragonViewer); /**
* mapDispatchToProps - used to hook up connect to action creators
* @memberof ManifestListItem
* @private
*/
const mapDispatchToProps = {
updateViewport: actions.updateViewport,
};
const enhance = compose(
connect(null, mapDispatchToProps),
miradorWithPlugins,
// further HOC go here
);
export default enhance(OpenSeadragonViewer);
import { compose } from 'redux'; import { compose } from 'redux';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import * as actions from '../state/actions';
import Workspace from '../components/Workspace'; import Workspace from '../components/Workspace';
/** /**
......
...@@ -21,6 +21,7 @@ const ActionTypes = { ...@@ -21,6 +21,7 @@ const ActionTypes = {
RECEIVE_INFO_RESPONSE_FAILURE: 'RECEIVE_INFO_RESPONSE_FAILURE', RECEIVE_INFO_RESPONSE_FAILURE: 'RECEIVE_INFO_RESPONSE_FAILURE',
REMOVE_INFO_RESPONSE: 'REMOVE_INFO_RESPONSE', REMOVE_INFO_RESPONSE: 'REMOVE_INFO_RESPONSE',
UPDATE_WORKSPACE_MOSAIC_LAYOUT: 'UPDATE_WORKSPACE_MOSAIC_LAYOUT', UPDATE_WORKSPACE_MOSAIC_LAYOUT: 'UPDATE_WORKSPACE_MOSAIC_LAYOUT',
UPDATE_VIEWPORT: 'UPDATE_VIEWPORT',
}; };
export default ActionTypes; export default ActionTypes;
...@@ -30,3 +30,14 @@ export function previousCanvas(windowId) { ...@@ -30,3 +30,14 @@ export function previousCanvas(windowId) {
export function setCanvas(windowId, canvasIndex) { export function setCanvas(windowId, canvasIndex) {
return { type: ActionTypes.SET_CANVAS, windowId, canvasIndex }; return { type: ActionTypes.SET_CANVAS, windowId, canvasIndex };
} }
/**
* updateViewport - action creator
*
* @param {String} windowId
* @param {Number} canvasIndex
* @memberof ActionCreators
*/
export function updateViewport(windowId, payload) {
return { type: ActionTypes.UPDATE_VIEWPORT, windowId, payload };
}
...@@ -36,6 +36,14 @@ const windowsReducer = (state = {}, action) => { ...@@ -36,6 +36,14 @@ const windowsReducer = (state = {}, action) => {
return setCanvasIndex(state, action.windowId, currentIndex => currentIndex - 1); return setCanvasIndex(state, action.windowId, currentIndex => currentIndex - 1);
case ActionTypes.SET_CANVAS: case ActionTypes.SET_CANVAS:
return setCanvasIndex(state, action.windowId, currentIndex => action.canvasIndex); return setCanvasIndex(state, action.windowId, currentIndex => action.canvasIndex);
case ActionTypes.UPDATE_VIEWPORT:
return {
...state,
[action.windowId]: {
...state[action.windowId],
viewer: action.payload,
},
};
default: default:
return state; return state;
} }
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment