UNPKG

@cogic/annotorious-openseadragon

Version:

Annotorious image annotation for OpenSeadragon

248 lines (189 loc) 8.64 kB
import OpenSeadragon from 'openseadragon'; import { drawShape } from '@cogic/annotorious/src/selectors'; import { format } from '@cogic/annotorious/src/util/Formatting'; import { addClass, removeClass } from '@cogic/annotorious/src/util/SVG'; import { isTouchDevice } from '@cogic/annotorious/src/util/Touch'; import { viewportTargetToImage, imageAnnotationToViewport, refreshViewportPosition } from '.'; import { AnnotationLayer } from '../OSDAnnotationLayer'; const isTouch = isTouchDevice(); export default class GigapixelAnnotationLayer extends AnnotationLayer { constructor(props) { super(props); this._initDrawingTools(true); } // Common code for fitBounds and fitBoundsWithConstraints _fit = (annotationOrId, opts, fn) => { const immediately = opts ? ( typeof opts == 'boolean' ? opts : opts.immediately ) : false; const padding = opts?.padding || 0; const shape = this.findShape(annotationOrId); if (shape) { const containerBounds = this.viewer.container.getBoundingClientRect(); const shapeBounds = shape.getBoundingClientRect(); const x = shapeBounds.x - containerBounds.x; const y = shapeBounds.y - containerBounds.y; const { width, height } = shapeBounds; const padX = x - padding; const padY = y - padding; const padW = width + 2 * padding; const padH = height + 2 * padding; const rect = this.viewer.viewport.viewerElementToViewportRectangle(new OpenSeadragon.Rect(padX, padY, padW, padH)); this.viewer.viewport[fn](rect, immediately); } } _getShapeAt = evt => { const getXY = evt => { if (isTouch) { const bbox = this.svg.getBoundingClientRect(); const x = evt.clientX - bbox.x; const y = evt.clientY - bbox.y; return new OpenSeadragon.Point(x, y); } else { return new OpenSeadragon.Point(evt.offsetX, evt.offsetY); } } // For some reason, this doesn't seem to work in one step... const pt = this.viewer.viewport.viewerElementToViewportCoordinates(getXY(evt)); const { x, y } = this.viewer.viewport.viewportToImageCoordinates(pt.x, pt.y); const annotation = this.store.getAnnotationAt(x, y, this.currentScale()); if (annotation) return this.findShape(annotation); } _refreshNonScalingAnnotations = () => { // No scaling needed in gigapixel mode! } addAnnotation = (annotation, optBuffer) => { const g = optBuffer || this.g; const shape = drawShape(annotation, this.env.image); addClass(shape, 'a9s-annotation'); shape.setAttribute('data-id', annotation.id); shape.annotation = annotation; refreshViewportPosition(this.viewer, shape); g.appendChild(shape); format(shape, annotation, this.formatters); return shape; } /** * Differs from non-gigapixel implementation only onsofar as * non-scaling shapes need no scaling! */ addOrUpdateAnnotation = (annotation, previous) => { const selected = this.selectedShape?.annotation; if (selected === annotation || selected?.isSelection || selected == previous) this.deselect(); if (previous) this.removeAnnotation(annotation); this.removeAnnotation(annotation); this.addAnnotation(annotation); this.store.insert(annotation); } /** Same: no counter-scaling for non-scaling shapes needed **/ deselect = () => { this.tools?.current.stop(); if (this.selectedShape) { const { annotation } = this.selectedShape; if (this.selectedShape.destroy) { // Modifiable shape: destroy and re-add the annotation this.selectedShape.mouseTracker.destroy(); this.selectedShape.destroy(); if (!annotation.isSelection) this.addAnnotation(annotation); } else { // Non-editable shape or read-only removeClass(this.selectedShape, 'selected'); } this.selectedShape = null; } } onDrawingComplete = shape => { // Annotation is in SVG coordinates - project to image coordinates const reprojected = shape.annotation.clone({ target: viewportTargetToImage(this.viewer, shape.annotation.target) }); shape.annotation = reprojected; this.selectShape(shape); this.emit('createSelection', shape.annotation); this.mouseTracker.enabled = false; } resize() { if (!this.store) return; // Update positions for all annotations except selected (will be handled separately) const shapes = Array.from(this.g.querySelectorAll('.a9s-annotation:not(.selected)')); shapes.forEach(s => refreshViewportPosition(this.viewer, s)); if (this.selectedShape) { if (this.selectedShape.element) { // Update the viewport position of the editable shape by transforming // this.selectedShape.element.annotation -> this always holds the current // position in image coordinates (including after drag/resize) const projected = imageAnnotationToViewport(this.viewer, this.selectedShape.element.annotation); this.selectedShape.updateState && this.selectedShape.updateState(projected); this.emit('viewportChange', this.selectedShape.element); } else { this.emit('viewportChange', this.selectedShape); } } } selectShape = (shape, skipEvent) => { if (!skipEvent && !shape.annotation.isSelection) this.emit('clickAnnotation', shape.annotation, shape); // Don't re-select if (this.selectedShape?.annotation === shape.annotation) return; // If another shape is currently selected, deselect first if (this.selectedShape && this.selectedShape.annotation !== shape.annotation) this.deselect(true); const { annotation } = shape; const readOnly = this.readOnly || annotation.readOnly; if (!(readOnly || this.headless)) { setTimeout(() => { shape.parentNode.removeChild(shape); // Fire the event AFTER the original shape was removed. Otherwise, // people calling `.getAnnotations()` in the `onSelectAnnotation` // handler will receive a duplicate annotation // (See issue https://github.com/recogito/annotorious-openseadragon/issues/63) if (!skipEvent) this.emit('select', { annotation, element: this.selectedShape.element }); }, 1); // Init the EditableShape (with the original annotation in image coordinates) const toolForAnnotation = this.tools.forAnnotation(annotation); this.selectedShape = toolForAnnotation.createEditableShape(annotation); this.selectedShape.element.annotation = annotation; // Instantly reproject the original annotation to viewport coorods const projected = imageAnnotationToViewport(this.viewer, annotation); this.selectedShape.updateState(projected); // Disable normal OSD nav const editableShapeMouseTracker = new OpenSeadragon.MouseTracker({ element: this.svg, preProcessEventHandler: info => { info.stopPropagation = true; info.preventDefault = false; info.preventGesture = true; } }).setTracking(false); // En-/disable OSD nav based on hover status this.selectedShape.element.addEventListener('mouseenter', () => { this.hoveredShape = this.selectedShape; editableShapeMouseTracker.setTracking(true); }); this.selectedShape.element.addEventListener('mouseleave', () => { this.hoveredShape = null; editableShapeMouseTracker.setTracking(false); }); this.selectedShape.mouseTracker = editableShapeMouseTracker; this.selectedShape.on('update', fragment => { // Fragment is in viewport coordinates - project back to image coords... const projectedTarget = viewportTargetToImage(this.viewer, fragment); // ...and update element.annotation, so everything stays in sync this.selectedShape.element.annotation = this.selectedShape.annotation.clone({ target: projectedTarget }); this.emit('updateTarget', this.selectedShape.element, projectedTarget) }); } else { this.selectedShape = shape; addClass(shape, 'selected'); if (!skipEvent) this.emit('select', { annotation, element: shape, skipEvent }); } } }