Skip to content
Snippets Groups Projects
Unverified Commit 794d09fb authored by Jessie Keck's avatar Jessie Keck Committed by GitHub
Browse files

Merge pull request #2955 from ProjectMirador/svg-annos

Add support for SvgSelectors in Mirador 3
parents bc253b93 58cf710b
No related branches found
No related tags found
No related merge requests found
{
"@context": "http://iiif.io/api/presentation/2/context.json",
"@type": "oa:Annotation",
"motivation": [
"oa:commenting"
],
"resource": [
{
"@type": "dctypes:Text",
"format": "text/html",
"chars": "<p>something</p>"
}
],
"on": {
"@type": "oa:SpecificResource",
"full": "https://oculus-dev.harvardx.harvard.edu/manifests/huam:320567/canvas/canvas-10466656.json",
"selector": {
"@type": "oa:Choice",
"default": {
"@type": "oa:FragmentSelector",
"value": "xywh=1000,219,198,148"
},
"item": {
"@type": "oa:SvgSelector",
"value": "<svg xmlns='http://www.w3.org/2000/svg'><path xmlns=\"http://www.w3.org/2000/svg\" d=\"M1000.24213,219.15375l98.78935,0l0,0l98.78935,0l0,74.09201l0,74.09201l-98.78935,0l-98.78935,0l0,-74.09201z\" data-paper-data=\"{&quot;defaultStrokeValue&quot;:1,&quot;editStrokeValue&quot;:5,&quot;currentStrokeValue&quot;:5,&quot;rotation&quot;:0,&quot;annotation&quot;:null,&quot;editable&quot;:true}\" id=\"rectangle_7e2b56fa-b18b-4d09-a575-0bb19f560b56\" fill-opacity=\"0\" fill=\"#00bfff\" fill-rule=\"nonzero\" stroke=\"#00bfff\" stroke-width=\"30.87167\" stroke-linecap=\"butt\" stroke-linejoin=\"miter\" stroke-miterlimit=\"10\" stroke-dasharray=\"\" stroke-dashoffset=\"0\" font-family=\"sans-serif\" font-weight=\"normal\" font-size=\"12\" text-anchor=\"start\" style=\"mix-blend-mode: normal\"/></svg>"
}
},
"within": {
"@id": "https://oculus-dev.harvardx.harvard.edu/manifests/huam:320567",
"@type": "sc:Manifest"
}
},
"@id": "d2eda2e2-951a-4f88-b4ec-03a7b25a5d07"
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<title>Mirador</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500">
</head>
<body>
<div id="mirador" style="position: absolute; top: 0; bottom: 0; left: 0; right: 0;"></div>
<script>document.write("<script type='text/javascript' src='../../../dist/mirador.min.js?v=" + Date.now() + "'><\/script>");</script>
<script type="text/javascript">
var miradorInstance = Mirador.viewer({
id: 'mirador',
windows: [
{
manifestId: 'https://api.myjson.com/bins/ahd5y',
},
{
manifestId: 'https://iiif.bodleian.ox.ac.uk/iiif/manifest/748a9d50-5a3a-440e-ab9d-567dd68b6abb.json',
canvasId: 'https://iiif.bodleian.ox.ac.uk/iiif/canvas/4a4d7347-0c32-4e3d-b517-5042eba06c25.json',
}
],
window: {
sideBarOpenByDefault: true,
defaultSideBarPanel: 'annotations'
},
});
</script>
</body>
</html>
......@@ -355,7 +355,7 @@ describe('OpenSeadragonViewer', () => {
wrapper.instance().annotationsToContext(annotations);
const context = wrapper.instance().osdCanvasOverlay.context2d;
expect(context.strokeStyle).toEqual('yellow');
expect(context.lineWidth).toEqual(4);
expect(context.lineWidth).toEqual(20);
expect(strokeRect).toHaveBeenCalledWith(10, 10, 100, 200);
});
});
......
......@@ -122,4 +122,20 @@ describe('AnnotationResource', () => {
.fragmentSelector).toEqual([10, 10, 100, 200]);
});
});
describe('svgSelector', () => {
it('simple string', () => {
expect(new AnnotationResource({ on: 'www.example.com/#xywh=10,10,100,200' })
.svgSelector).toEqual(null);
});
it('array of selectors', () => {
expect(new AnnotationResource({ on: [{ selector: { item: { '@type': 'oa:SvgSelector' } } }] })
.svgSelector).toEqual({ '@type': 'oa:SvgSelector' });
});
it('without specified type', () => {
expect(new AnnotationResource({ on: [{ selector: { item: {} } }] })
.svgSelector).toEqual(null);
});
});
});
import CanvasAnnotationDisplay from '../../../src/lib/CanvasAnnotationDisplay';
import AnnotationResource from '../../../src/lib/AnnotationResource';
import dualStrategyAnno from '../../fixtures/version-2/annotationMiradorDual.json';
/** */
function createSubject(args) {
return new CanvasAnnotationDisplay({
color: 'blue',
offset: {
x: -100,
y: 0,
},
zoom: 0.0005,
...args,
});
}
describe('CanvasAnnotationDisplay', () => {
describe('toContext', () => {
it('selects svgSelector if present in a dual anno', () => {
const context = {
stroke: jest.fn(),
};
const subject = createSubject({
resource: new AnnotationResource(dualStrategyAnno),
});
subject.svgContext = jest.fn();
subject.fragmentContext = jest.fn();
subject.toContext(context);
expect(subject.svgContext).toHaveBeenCalled();
expect(subject.fragmentContext).not.toHaveBeenCalled();
});
it('selects fragmentSelector if no svg present', () => {
const context = {
stroke: jest.fn(),
};
const subject = createSubject({
resource: new AnnotationResource({ on: 'www.example.com/#xywh=10,10,100,200' }),
});
subject.svgContext = jest.fn();
subject.fragmentContext = jest.fn();
subject.toContext(context);
expect(subject.svgContext).not.toHaveBeenCalled();
expect(subject.fragmentContext).toHaveBeenCalled();
});
});
describe('svgString', () => {
it('selects the svg selector string value', () => {
const subject = createSubject({
resource: new AnnotationResource(dualStrategyAnno),
});
expect(subject.svgString).toMatch(/<svg/);
});
});
describe('svgContext', () => {
it('draws the paths with selected arguments', () => {
const context = {
stroke: jest.fn(),
};
const subject = createSubject({
resource: new AnnotationResource(dualStrategyAnno),
});
subject.svgContext(context);
expect(context.stroke).toHaveBeenCalledWith({});
expect(context.strokeStyle).toEqual('blue');
expect(context.lineWidth).toEqual(20);
});
});
describe('fragmentContext', () => {
it('draws the fragment with selected arguments', () => {
const context = {
strokeRect: jest.fn(),
};
const subject = createSubject({
resource: new AnnotationResource({ on: 'www.example.com/#xywh=10,10,100,200' }),
});
subject.fragmentContext(context);
expect(context.strokeRect).toHaveBeenCalledWith(-90, 10, 100, 200);
expect(context.strokeStyle).toEqual('blue');
expect(context.lineWidth).toEqual(20);
});
});
});
......@@ -32,6 +32,11 @@ class IntersectionObserverPolyfill {
global.IntersectionObserver = IntersectionObserverPolyfill;
/** */
function Path2D() {
}
global.Path2D = Path2D;
/**
* copy object property descriptors from `src` to `target`
* @param {*} src
......
......@@ -6,6 +6,7 @@ import classNames from 'classnames';
import ns from '../config/css-ns';
import OpenSeadragonCanvasOverlay from '../lib/OpenSeadragonCanvasOverlay';
import CanvasWorld from '../lib/CanvasWorld';
import CanvasAnnotationDisplay from '../lib/CanvasAnnotationDisplay';
/**
* Represents a OpenSeadragonViewer in the mirador workspace. Responsible for mounting
......@@ -183,16 +184,14 @@ export class OpenSeadragonViewer extends Component {
const { canvasWorld } = this.props;
const context = this.osdCanvasOverlay.context2d;
const zoom = this.viewer.viewport.getZoom(true);
const width = canvasWorld.worldBounds()[2];
annotations.forEach((annotation) => {
annotation.resources.forEach((resource) => {
if (!canvasWorld.canvasIds.includes(resource.targetId)) return;
const offset = canvasWorld.offsetByCanvas(resource.targetId);
const fragment = resource.fragmentSelector;
fragment[0] += offset.x;
context.strokeStyle = color;
context.lineWidth = Math.ceil(10 / (zoom * width));
context.strokeRect(...fragment);
const canvasAnnotationDisplay = new CanvasAnnotationDisplay({
color, offset, resource, zoom,
});
canvasAnnotationDisplay.toContext(context);
});
});
}
......
......@@ -68,6 +68,23 @@ export default class AnnotationResource {
}
}
/** */
get svgSelector() {
const on = this.on[0];
switch (typeof on) {
case 'string':
return null;
case 'object':
if (on.selector && on.selector.item && on.selector.item['@type'] === 'oa:SvgSelector') {
return on.selector.item;
}
return null;
default:
return null;
}
}
/** */
get fragmentSelector() {
const { selector } = this;
......
/**
* CanvasAnnotationDisplay - class used to display a SVG and fragment based
* annotations.
*/
export default class CanvasAnnotationDisplay {
/** */
constructor({
resource, color, zoom, offset,
}) {
this.resource = resource;
this.color = color;
this.zoom = zoom;
this.offset = offset;
}
/** */
toContext(context) {
if (this.resource.svgSelector) {
this.svgContext(context);
} else {
this.fragmentContext(context);
}
}
/** */
get svgString() {
return this.resource.svgSelector.value;
}
/** */
svgContext(context) {
[...this.svgPaths].forEach((element) => {
/**
* Note: Path2D is not supported in IE11.
* TODO: Support multi canvas offset
* One example: https://developer.mozilla.org/en-US/docs/Web/API/Path2D/addPath
*/
const p = new Path2D(element.attributes.d.nodeValue);
/**
* Note: we could do something to return the svg styling attributes as
* some have encoded information in these values. However, how should we
* handle highlighting and other complications?
* context.strokeStyle = element.attributes.stroke.nodeValue;
* context.lineWidth = element.attributes['stroke-width'].nodeValue;
*/
context.strokeStyle = this.color; // eslint-disable-line no-param-reassign
context.lineWidth = this.lineWidth(); // eslint-disable-line no-param-reassign
context.stroke(p);
});
}
/** */
fragmentContext(context) {
const fragment = this.resource.fragmentSelector;
fragment[0] += this.offset.x;
context.strokeStyle = this.color; // eslint-disable-line no-param-reassign
context.lineWidth = this.lineWidth(); // eslint-disable-line no-param-reassign
context.strokeRect(...fragment);
}
/** */
lineWidth() {
return Math.ceil(1 / (this.zoom * 100));
}
/** */
get svgPaths() {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(this.svgString, 'text/xml');
return xmlDoc.getElementsByTagName('path');
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment