@cogic/annotorious-openseadragon
Version:
Annotorious image annotation for OpenSeadragon
196 lines (162 loc) • 6.16 kB
JavaScript
import RBush from 'rbush';
import { SVG_NAMESPACE } from '@cogic/annotorious/src/util/SVG';
import { drawShape, shapeArea, svgFragmentToShape, parseRectFragment } from '@cogic/annotorious/src/selectors';
import { WebAnnotation } from '@recogito/recogito-client-core';
import {
pointInCircle,
pointInEllipse,
pointInPolygon,
svgPathToPolygons,
pointInLine
} from '@cogic/annotorious/src/util/Geom2D';
/**
* Computes the bounding box of an annotation. WARNING:
* this is an expensive operation which parses the annotation,
* creates a temporary SVG element and attaches it to the DOM,
* uses .getBBox() and then removes the temporary SVG element.
*/
const getBounds = (annotation, image) => {
const isBox = annotation.targets[0].selector.type === 'FragmentSelector';
if (isBox) {
const {x,y,w,h} = parseRectFragment(annotation, image);
return {
minX: x,
minY: y,
maxX: x + w,
maxY: y + h
};
} else {
const shape = drawShape(annotation, image);
// A temporary SVG buffer, so we can use .getBBox()
const svg = document.createElementNS(SVG_NAMESPACE, 'svg');
svg.style.position = 'absolute';
svg.style.opacity = 0;
svg.style.top = 0;
svg.style.left = 0;
svg.appendChild(shape);
document.body.appendChild(svg);
const { x, y, width, height } = shape.getBBox();
document.body.removeChild(svg);
return {
minX: x,
minY: y,
maxX: x + width,
maxY: y + height
};
}
}
const getSelectorType = annotation => {
const firstTarget = annotation.targets[0];
return Array.isArray(firstTarget.selector) ?
firstTarget.selector[0].type : firstTarget.selector?.type;
}
/**
* Checks if a point is inside the annotation shape.
* WARNING: this is only for internal use ONLY. It pre-assumes
* an annotation with an SVGSelector (not a FragmentSelector)!
* @param {number} x point x coordinate
* @param {number} y point y coordinate
* @param {WebAnnotation} annotation annotation (with an SVG Selector!)
*/
const pointInSVGShape = (x, y, annotation, buffer) => {
const svg = svgFragmentToShape(annotation);
const nodeName = svg.nodeName.toLowerCase();
const pt = [x, y];
if (nodeName === 'polygon') {
const points = Array.from(svg.points).map(pt => [pt.x, pt.y]);
return pointInPolygon(pt, points);
} else if (nodeName === 'circle') {
const cx = svg.getAttribute('cx');
const cy = svg.getAttribute('cy');
const r = svg.getAttribute('r');
return pointInCircle(pt, cx, cy, r);
} else if (nodeName === 'ellipse') {
const cx = svg.getAttribute('cx');
const cy = svg.getAttribute('cy');
const rx = svg.getAttribute('rx');
const ry = svg.getAttribute('ry');
return pointInEllipse(pt, cx, cy, rx, ry);
} else if (nodeName === 'path') {
const polygons = svgPathToPolygons(svg);
return polygons.find(polygon => pointInPolygon(pt, polygon));
} else if (nodeName === 'line') {
const x1 = parseInt(svg.getAttribute('x1'));
const y1 = parseInt(svg.getAttribute('y1'));
const x2 = parseInt(svg.getAttribute('x2'));
const y2 = parseInt(svg.getAttribute('y2'));
return pointInLine(pt, x1, y1, x2, y2, buffer);
} else {
throw `Unsupported SVG shape type: ${nodeName}`;
}
}
export default class AnnotationStore {
constructor(env) {
this.env = env;
// Hacky... the store exposes itself to the environment, so that tools
// have access to it
env.store = this;
this.spatial_index = new RBush();
}
clear = () =>
this.spatial_index.clear();
getAnnotationAt = (x, y, scale, excludedAnnotationIds = []) => {
// 5 pixel buffer, so we reliably catch point
// annotations (optionally with scale applied)
const buffer = scale ? 5 / scale : 5;
// Fast hit test in index (bounds only!)
const idxHits = this.spatial_index.search({
minX: x - buffer,
minY: y - buffer,
maxX: x + buffer,
maxY: y + buffer
}).map(item => item.annotation);
// Exact hit test on shape (needed for SVG fragments only!)
const exactHits = idxHits.filter(annotation => {
if (excludedAnnotationIds.includes(annotation.id)) {
return false;
}
const selectorType = getSelectorType(annotation);
if (selectorType === 'FragmentSelector') {
return true; // For FragmentSelectors, shape is always equal to bounds!
} else if (selectorType === 'SvgSelector') {
return pointInSVGShape(x, y, annotation, buffer);
} else {
throw `Unsupported selector type: ${selectorType}`;
}
});
// Get smallest annotation
if (exactHits.length > 0) {
exactHits.sort((a, b) => shapeArea(a, this.env.image) - shapeArea(b, this.env.image));
return exactHits[0];
}
}
getAnnotationsIntersecting = annotationOrBounds => {
const isBounds = annotationOrBounds.minX; // Bit of a naive test...
const bounds = isBounds ? annotationOrBounds : getBounds(annotationOrBounds, this.env.image);
const intersecting = this.spatial_index.search(bounds).map(item => item.annotation);
return isBounds ? intersecting :
intersecting.filter(a => !a.isEqual(annotationOrBounds));
}
insert = arg => {
const annotations = Array.isArray(arg) ? arg : [ arg ];
annotations.forEach(annotation => {
this.spatial_index.insert({
...getBounds(annotation, this.env.image), annotation
})
});
}
getBounds = annotation => {
return this.getBounds(annotation, this.env.image);
}
remove = annotation => {
// Unfortunately, .remove currently requires bounds,
// therefore we need to re-compute. See:
// https://github.com/mourner/rbush/issues/95
const item = {
...getBounds(annotation, this.env.image),
annotation
};
this.spatial_index.remove(item, (a, b) =>
a.annotation.id === b.annotation.id);
}
}