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

Sets up OpenSeadragonViewer to display provided annotations for

a single canvas
parent 28b5784d
No related branches found
No related tags found
No related merge requests found
...@@ -2,15 +2,20 @@ import React from 'react'; ...@@ -2,15 +2,20 @@ import React from 'react';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import OpenSeadragon from 'openseadragon'; import OpenSeadragon from 'openseadragon';
import { OpenSeadragonViewer } from '../../../src/components/OpenSeadragonViewer'; import { OpenSeadragonViewer } from '../../../src/components/OpenSeadragonViewer';
import OpenSeadragonCanvasOverlay from '../../../src/lib/OpenSeadragonCanvasOverlay';
import Annotation from '../../../src/lib/Annotation';
import ZoomControls from '../../../src/containers/ZoomControls'; import ZoomControls from '../../../src/containers/ZoomControls';
jest.mock('openseadragon'); jest.mock('openseadragon');
jest.mock('../../../src/lib/OpenSeadragonCanvasOverlay');
describe('OpenSeadragonViewer', () => { describe('OpenSeadragonViewer', () => {
let wrapper; let wrapper;
let updateViewport; let updateViewport;
beforeEach(() => { beforeEach(() => {
OpenSeadragon.mockClear(); OpenSeadragon.mockClear();
OpenSeadragonCanvasOverlay.mockClear();
updateViewport = jest.fn(); updateViewport = jest.fn();
...@@ -126,6 +131,16 @@ describe('OpenSeadragonViewer', () => { ...@@ -126,6 +131,16 @@ describe('OpenSeadragonViewer', () => {
0.5, { x: 1, y: 0, zoom: 0.5 }, false, 0.5, { x: 1, y: 0, zoom: 0.5 }, false,
); );
}); });
it('sets up a OpenSeadragonCanvasOverlay', () => {
wrapper.instance().componentDidMount();
expect(OpenSeadragonCanvasOverlay).toHaveBeenCalledTimes(1);
});
it('sets up a listener on update-viewport', () => {
wrapper.instance().componentDidMount();
expect(addHandler).toHaveBeenCalledWith('update-viewport', expect.anything());
});
}); });
describe('componentDidUpdate', () => { describe('componentDidUpdate', () => {
...@@ -153,6 +168,49 @@ describe('OpenSeadragonViewer', () => { ...@@ -153,6 +168,49 @@ describe('OpenSeadragonViewer', () => {
0.5, { x: 1, y: 0, zoom: 0.5 }, false, 0.5, { x: 1, y: 0, zoom: 0.5 }, false,
); );
}); });
it('sets up canvasUpdate to add annotations to the canvas', () => {
const clear = jest.fn();
const resize = jest.fn();
const canvasUpdate = jest.fn();
wrapper.instance().osdCanvasOverlay = {
clear,
resize,
canvasUpdate,
};
wrapper.setProps(
{
annotations: [
new Annotation(
{ '@id': 'foo', resources: [{ foo: 'bar' }] },
),
],
},
);
wrapper.setProps(
{
annotations: [
new Annotation(
{ '@id': 'foo', resources: [{ foo: 'bar' }] },
),
],
},
);
wrapper.setProps(
{
annotations: [
new Annotation(
{ '@id': 'bar', resources: [{ foo: 'bar' }] },
),
],
},
);
wrapper.instance().updateCanvas();
expect(clear).toHaveBeenCalledTimes(1);
expect(resize).toHaveBeenCalledTimes(1);
expect(canvasUpdate).toHaveBeenCalledTimes(1);
});
}); });
describe('onViewportChange', () => { describe('onViewportChange', () => {
...@@ -173,4 +231,35 @@ describe('OpenSeadragonViewer', () => { ...@@ -173,4 +231,35 @@ describe('OpenSeadragonViewer', () => {
); );
}); });
}); });
describe('onUpdateViewport', () => {
it('fires updateCanvas', () => {
const updateCanvas = jest.fn();
wrapper.instance().updateCanvas = updateCanvas;
wrapper.instance().onUpdateViewport();
expect(updateCanvas).toHaveBeenCalledTimes(1);
});
});
describe('annotationsToContext', () => {
it('converts the annotations to canvas', () => {
const strokeRect = jest.fn();
wrapper.instance().osdCanvasOverlay = {
context2d: {
strokeRect,
},
};
const annotations = [
new Annotation(
{ '@id': 'foo', resources: [{ on: 'www.example.com/#xywh=10,10,100,200' }] },
),
];
wrapper.instance().annotationsToContext(annotations);
const context = wrapper.instance().osdCanvasOverlay.context2d;
expect(context.strokeStyle).toEqual('yellow');
expect(context.lineWidth).toEqual(10);
expect(strokeRect).toHaveBeenCalledWith(10, 10, 100, 200);
});
});
}); });
import OpenSeadragon from 'openseadragon';
import OpenSeadragonCanvasOverlay from '../../../src/lib/OpenSeadragonCanvasOverlay';
jest.mock('openseadragon');
describe('OpenSeadragonCanvasOverlay', () => {
let canvasOverlay;
beforeEach(() => {
document.body.innerHTML = '<div id="canvas"></div>';
OpenSeadragon.mockClear();
OpenSeadragon.mockImplementation(() => ({
canvas: document.getElementById('canvas'),
container: {
clientHeight: 100,
clientWidth: 200,
},
viewport: {
getBounds: jest.fn(() => ({
x: 40, y: 80, width: 200, height: 300,
})),
getZoom: jest.fn(() => (0.75)),
},
world: {
getItemAt: jest.fn(() => ({
source: {
dimensions: {
x: 1000,
y: 2000,
},
},
viewportToImageZoom: jest.fn(() => (0.075)),
})),
},
}));
canvasOverlay = new OpenSeadragonCanvasOverlay(new OpenSeadragon());
});
describe('constructor', () => {
it('sets up initial values and canvas', () => {
expect(canvasOverlay.containerHeight).toEqual(0);
expect(canvasOverlay.containerWidth).toEqual(0);
expect(canvasOverlay.canvasDiv.outerHTML).toEqual(
'<div style="position: absolute; left: 0px; top: 0px; width: 100%; height: 100%;"><canvas></canvas></div>',
);
});
});
describe('context2d', () => {
it('calls getContext on canvas', () => {
const contextMock = jest.fn();
canvasOverlay.canvas = {
getContext: contextMock,
};
canvasOverlay.context2d; // eslint-disable-line no-unused-expressions
expect(contextMock).toHaveBeenCalledTimes(1);
});
});
describe('clear', () => {
it('calls getContext and clearRect on canvas', () => {
const clearRect = jest.fn();
const contextMock = jest.fn(() => ({
clearRect,
}));
canvasOverlay.canvas = {
getContext: contextMock,
};
canvasOverlay.clear();
expect(contextMock).toHaveBeenCalledTimes(1);
expect(clearRect).toHaveBeenCalledTimes(1);
});
});
describe('resize', () => {
it('sets various values based off of image and container sizes', () => {
canvasOverlay.resize();
expect(canvasOverlay.containerHeight).toEqual(100);
expect(canvasOverlay.containerWidth).toEqual(200);
expect(canvasOverlay.imgAspectRatio).toEqual(0.5);
});
it('when image is undefined returns early', () => {
OpenSeadragon.mockClear();
OpenSeadragon.mockImplementation(() => ({
canvas: document.getElementById('canvas'),
container: {
clientHeight: 100,
clientWidth: 200,
},
viewport: {
getBounds: jest.fn(() => (new OpenSeadragon.Rect(0, 0, 200, 200))),
},
world: {
getItemAt: jest.fn(),
},
}));
canvasOverlay = new OpenSeadragonCanvasOverlay(new OpenSeadragon());
canvasOverlay.resize();
expect(canvasOverlay.imgHeight).toEqual(undefined);
expect(canvasOverlay.imgWidth).toEqual(undefined);
});
});
describe('canvasUpdate', () => {
it('sets appropriate sizes and calls update argument', () => {
const scale = jest.fn();
const setAttribute = jest.fn();
const setTransform = jest.fn();
const translate = jest.fn();
const contextMock = jest.fn(() => ({
scale,
setTransform,
translate,
}));
canvasOverlay.canvas = {
getContext: contextMock,
setAttribute,
};
const update = jest.fn();
canvasOverlay.resize();
canvasOverlay.canvasUpdate(update);
expect(update).toHaveBeenCalledTimes(1);
expect(scale).toHaveBeenCalledWith(0.075, 0.075);
expect(translate).toHaveBeenCalledWith(-39.96, -26.65333333333333);
expect(setTransform).toHaveBeenCalledWith(1, 0, 0, 1, 0, 0);
});
});
});
...@@ -4,6 +4,7 @@ import OpenSeadragon from 'openseadragon'; ...@@ -4,6 +4,7 @@ import OpenSeadragon from 'openseadragon';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import ns from '../config/css-ns'; import ns from '../config/css-ns';
import ZoomControls from '../containers/ZoomControls'; import ZoomControls from '../containers/ZoomControls';
import OpenSeadragonCanvasOverlay from '../lib/OpenSeadragonCanvasOverlay';
/** /**
* Represents a OpenSeadragonViewer in the mirador workspace. Responsible for mounting * Represents a OpenSeadragonViewer in the mirador workspace. Responsible for mounting
...@@ -17,7 +18,11 @@ export class OpenSeadragonViewer extends Component { ...@@ -17,7 +18,11 @@ export class OpenSeadragonViewer extends Component {
super(props); super(props);
this.viewer = null; this.viewer = null;
this.osdCanvasOverlay = null;
// An initial value for the updateCanvas method
this.updateCanvas = () => {};
this.ref = React.createRef(); this.ref = React.createRef();
this.onUpdateViewport = this.onUpdateViewport.bind(this);
this.onViewportChange = this.onViewportChange.bind(this); this.onViewportChange = this.onViewportChange.bind(this);
} }
...@@ -37,6 +42,9 @@ export class OpenSeadragonViewer extends Component { ...@@ -37,6 +42,9 @@ export class OpenSeadragonViewer extends Component {
showNavigationControl: false, showNavigationControl: false,
preserveImageSizeOnResize: true, preserveImageSizeOnResize: true,
}); });
this.osdCanvasOverlay = new OpenSeadragonCanvasOverlay(this.viewer);
this.viewer.addHandler('update-viewport', this.onUpdateViewport);
this.viewer.addHandler('viewport-change', debounce(this.onViewportChange, 300)); this.viewer.addHandler('viewport-change', debounce(this.onViewportChange, 300));
if (viewer) { if (viewer) {
...@@ -49,10 +57,21 @@ export class OpenSeadragonViewer extends Component { ...@@ -49,10 +57,21 @@ export class OpenSeadragonViewer extends Component {
/** /**
* When the tileSources change, make sure to close the OSD viewer. * When the tileSources change, make sure to close the OSD viewer.
* When the annotations change, reset the updateCanvas method to make sure
* they are added.
* When the viewport state changes, pan or zoom the OSD viewer as appropriate * When the viewport state changes, pan or zoom the OSD viewer as appropriate
*/ */
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
const { tileSources, viewer } = this.props; const { tileSources, viewer, annotations } = this.props;
if (!this.annotationsMatch(prevProps.annotations)) {
this.updateCanvas = () => {
this.osdCanvasOverlay.clear();
this.osdCanvasOverlay.resize();
this.osdCanvasOverlay.canvasUpdate(() => {
this.annotationsToContext(annotations);
});
};
}
if (!this.tileSourcesMatch(prevProps.tileSources)) { if (!this.tileSourcesMatch(prevProps.tileSources)) {
this.viewer.close(); this.viewer.close();
Promise.all( Promise.all(
...@@ -82,6 +101,13 @@ export class OpenSeadragonViewer extends Component { ...@@ -82,6 +101,13 @@ export class OpenSeadragonViewer extends Component {
this.viewer.removeAllHandlers(); this.viewer.removeAllHandlers();
} }
/**
* onUpdateViewport - fires during OpenSeadragon render method.
*/
onUpdateViewport(event) {
this.updateCanvas();
}
/** /**
* Forward OSD state to redux * Forward OSD state to redux
*/ */
...@@ -97,6 +123,20 @@ export class OpenSeadragonViewer extends Component { ...@@ -97,6 +123,20 @@ export class OpenSeadragonViewer extends Component {
}); });
} }
/**
* annotationsToContext - converts anontations to a canvas context
*/
annotationsToContext(annotations) {
const context = this.osdCanvasOverlay.context2d;
annotations.forEach((annotation) => {
annotation.resources.forEach((resource) => {
context.strokeStyle = 'yellow';
context.lineWidth = 10;
context.strokeRect(...resource.fragmentSelector);
});
});
}
/** /**
* boundsFromTileSources - calculates the overall width/height * boundsFromTileSources - calculates the overall width/height
* based on 0 -> n tileSources * based on 0 -> n tileSources
...@@ -196,6 +236,25 @@ export class OpenSeadragonViewer extends Component { ...@@ -196,6 +236,25 @@ export class OpenSeadragonViewer extends Component {
}); });
} }
/**
* annotationsMatch - compares previous annotations to current to determine
* whether to add a new updateCanvas method to draw annotations
* @param {Array} prevAnnotations
* @return {Boolean}
*/
annotationsMatch(prevAnnotations) {
const { annotations } = this.props;
return annotations.some((annotation, index) => {
if (!prevAnnotations[index]) {
return false;
}
if (annotation.id === prevAnnotations[index].id) {
return true;
}
return false;
});
}
/** /**
* Renders things * Renders things
*/ */
...@@ -221,6 +280,7 @@ export class OpenSeadragonViewer extends Component { ...@@ -221,6 +280,7 @@ export class OpenSeadragonViewer extends Component {
} }
OpenSeadragonViewer.defaultProps = { OpenSeadragonViewer.defaultProps = {
annotations: [],
children: null, children: null,
tileSources: [], tileSources: [],
viewer: null, viewer: null,
...@@ -228,6 +288,7 @@ OpenSeadragonViewer.defaultProps = { ...@@ -228,6 +288,7 @@ OpenSeadragonViewer.defaultProps = {
}; };
OpenSeadragonViewer.propTypes = { OpenSeadragonViewer.propTypes = {
annotations: PropTypes.arrayOf(PropTypes.object),
children: PropTypes.element, children: PropTypes.element,
tileSources: PropTypes.arrayOf(PropTypes.object), tileSources: PropTypes.arrayOf(PropTypes.object),
viewer: PropTypes.object, // eslint-disable-line react/forbid-prop-types viewer: PropTypes.object, // eslint-disable-line react/forbid-prop-types
......
...@@ -126,6 +126,7 @@ export class WindowViewer extends Component { ...@@ -126,6 +126,7 @@ export class WindowViewer extends Component {
<> <>
<OSDViewer <OSDViewer
tileSources={this.tileInfoFetchedFromStore()} tileSources={this.tileInfoFetchedFromStore()}
currentCanvases={this.currentCanvases()}
windowId={window.id} windowId={window.id}
> >
<ViewerNavigation window={window} canvases={this.canvases} /> <ViewerNavigation window={window} canvases={this.canvases} />
......
...@@ -7,6 +7,7 @@ import * as actions from '../state/actions'; ...@@ -7,6 +7,7 @@ import * as actions from '../state/actions';
import { import {
getCanvasLabel, getCanvasLabel,
getSelectedCanvas, getSelectedCanvas,
getSelectedCanvasAnnotations,
} from '../state/selectors'; } from '../state/selectors';
/** /**
...@@ -14,12 +15,18 @@ import { ...@@ -14,12 +15,18 @@ import {
* @memberof Window * @memberof Window
* @private * @private
*/ */
const mapStateToProps = ({ viewers, windows, manifests }, { windowId }) => ({ const mapStateToProps = ({
viewers, windows, manifests, annotations,
}, { windowId, currentCanvases }) => ({
viewer: viewers[windowId], viewer: viewers[windowId],
label: getCanvasLabel( label: getCanvasLabel(
getSelectedCanvas({ windows, manifests }, windowId), getSelectedCanvas({ windows, manifests }, windowId),
windows[windowId].canvasIndex, windows[windowId].canvasIndex,
), ),
annotations: getSelectedCanvasAnnotations(
{ annotations },
currentCanvases.map(canvas => canvas.id),
),
}); });
/** /**
......
import OpenSeadragon from 'openseadragon';
/**
* OpenSeadragonCanvasOverlay - adapted from https://github.com/altert/OpenSeadragonCanvasOverlay
* used rather than an "onRedraw" function we tap into our own method. Existing
* repository is not published as an npm package.
* Code ported from https://github.com/altert/OpenSeadragonCanvasOverlay
* carries a BSD 3-Clause license originally authored by @altert from
* https://github.com/altert/OpenseadragonFabricjsOverlay
*/
export default class OpenSeadragonCanvasOverlay {
/**
* constructor - sets up the Canvas overlay container
*/
constructor(viewer) {
this.viewer = viewer;
this.containerWidth = 0;
this.containerHeight = 0;
this.canvasDiv = document.createElement('div');
this.canvasDiv.style.position = 'absolute';
this.canvasDiv.style.left = 0;
this.canvasDiv.style.top = 0;
this.canvasDiv.style.width = '100%';
this.canvasDiv.style.height = '100%';
this.viewer.canvas.appendChild(this.canvasDiv);
this.canvas = document.createElement('canvas');
this.canvasDiv.appendChild(this.canvas);
this.imgAspectRatio = 1;
}
/** */
get context2d() {
return this.canvas.getContext('2d');
}
/** */
clear() {
this.canvas.getContext('2d').clearRect(0, 0, this.containerWidth, this.containerHeight);
}
/**
* resize - resizes the added Canvas overlay.
*/
resize() {
if (this.containerWidth !== this.viewer.container.clientWidth) {
this.containerWidth = this.viewer.container.clientWidth;
this.canvasDiv.setAttribute('width', this.containerWidth);
this.canvas.setAttribute('width', this.containerWidth);
}
if (this.containerHeight !== this.viewer.container.clientHeight) {
this.containerHeight = this.viewer.container.clientHeight;
this.canvasDiv.setAttribute('height', this.containerHeight);
this.canvas.setAttribute('height', this.containerHeight);
}
this.viewportOrigin = new OpenSeadragon.Point(0, 0);
const boundsRect = this.viewer.viewport.getBounds(true);
this.viewportOrigin.x = boundsRect.x;
this.viewportOrigin.y = boundsRect.y * this.imgAspectRatio;
this.viewportWidth = boundsRect.width;
this.viewportHeight = boundsRect.height * this.imgAspectRatio;
const image1 = this.viewer.world.getItemAt(0);
if (!image1) return;
this.imgWidth = image1.source.dimensions.x;
this.imgHeight = image1.source.dimensions.y;
this.imgAspectRatio = this.imgWidth / this.imgHeight;
}
/**
* canvasUpdate - sets up the dimensions for the canvas update to mimick image
* 0 dimensions. Then call provided update function.
* @param {Function} update
*/
canvasUpdate(update) {
const viewportZoom = this.viewer.viewport.getZoom(true);
const image1 = this.viewer.world.getItemAt(0);
if (!image1) return;
const zoom = image1.viewportToImageZoom(viewportZoom);
const x = (
(this.viewportOrigin.x / this.imgWidth - this.viewportOrigin.x) / this.viewportWidth
) * this.containerWidth;
const y = (
(this.viewportOrigin.y / this.imgHeight - this.viewportOrigin.y) / this.viewportHeight
) * this.containerHeight;
if (this.clearBeforeRedraw) this.clear();
this.canvas.getContext('2d').translate(x, y);
this.canvas.getContext('2d').scale(zoom, zoom);
update();
this.canvas.getContext('2d').setTransform(1, 0, 0, 1, 0, 0);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment