UNPKG

labo-components

Version:
334 lines (282 loc) 13.3 kB
import React from 'react'; import PropTypes from "prop-types"; import IDUtil from '../../../util/IDUtil'; import IconUtil from '../../../util/IconUtil'; import MediaEvents from '../_MediaEvents'; import { AnnotationEvents } from '../AnnotationClient'; import { ResourceViewerContext } from '../ResourceViewerContext'; import classNames from 'classnames'; //TODO only add a function to enforce adding a NEW annotation //TODO test/implement highlighting the initial search result //TODO prevent zooming when clicking on the overlay export default class ImageViewer extends React.PureComponent { static contextType = ResourceViewerContext; constructor(props) { super(props); this.viewer = null; this.selector = null; //needed to access picturae selector this.annotationOverlays = []; this.CLASS_PREFIX = 'imv'; this.SEARCH_TERM_OVERLAY_ID = 'search-term-highlight'; this.highlightOverlays = []; this.OVERLAY_TIMEOUT = 300; //FIXME find the right event to know when it's ok to draw an overlay } componentDidMount = async() => { await this.initViewer(); this.viewer.removeAllHandlers('open'); this.__showSearchHighlight(this.context.activeMediaObject); this.props.annotationClient.events.bind(AnnotationEvents.ON_SET_ANNOTATION, this.onSetActiveAnnotation); this.props.annotationClient.events.bind(AnnotationEvents.ON_CHANGE_TARGET, this.onChangeTarget); this.props.annotationClient.events.bind(AnnotationEvents.ON_DELETE, this.onDeleteAnnotation); this.props.annotationClient.events.bind(AnnotationEvents.ON_SAVE, this.onSaveAnnotation); this.context.mediaEvents.bind(MediaEvents.ACTIVE_MEDIA_OBJECT, this.onSetMediaObject); }; componentWillUnmount = () => { this.props.annotationClient.events.unbind(AnnotationEvents.ON_SET_ANNOTATION, this.onSetActiveAnnotation); this.props.annotationClient.events.unbind(AnnotationEvents.ON_CHANGE_TARGET, this.onChangeTarget); this.props.annotationClient.events.unbind(AnnotationEvents.ON_DELETE, this.onDeleteAnnotation); this.props.annotationClient.events.unbind(AnnotationEvents.ON_SAVE, this.onSaveAnnotation); this.context.mediaEvents.unbind(MediaEvents.ACTIVE_MEDIA_OBJECT, this.onSetMediaObject); }; /* -------------------------------------------------------------- -------------------------- VIEWER INITIALIZATION ---------------- ---------------------------------------------------------------*/ __toOSDUrl = mediaObject => { const index = mediaObject.url.indexOf('.tif'); //FIXME very weak way to check if it's IIIF! let moClone = JSON.parse(JSON.stringify(mediaObject)); if(index === -1) { moClone.infoUrl = mediaObject.url; } else { moClone.infoUrl = mediaObject.url.substring(0, index + 4) + '/info.json'; } return moClone; }; __showSearchHighlight = mediaObject => { //always remove the old highlights this.highlightOverlays.forEach(o => { this.viewer.removeOverlay(o); }); this.highlightOverlays = []; //only continue if there is data if(!mediaObject || !mediaObject.mediaFragments) return; //draw the highlight overlays (and store their ids for reference) for( let i = 0; i < mediaObject.mediaFragments.length; i++){ const r = this.viewer.viewport.imageToViewportRectangle( parseInt(mediaObject.mediaFragments[i].x), parseInt(mediaObject.mediaFragments[i].y), parseInt(mediaObject.mediaFragments[i].w), parseInt(mediaObject.mediaFragments[i].h) ); const elt = document.createElement("div"); elt.id = IDUtil.guid(); elt.className = IDUtil.cssClassName('highlight', this.CLASS_PREFIX); this.highlightOverlays.push(elt.id + ''); this.viewer.addOverlay(elt, r); } }; __getPageNumberForMediaObject = mediaObject => mediaObject ? this.props.mediaObjects.findIndex( mo => mo.contentId == mediaObject.contentId ) : 0; //triggered only whenever the media object was changed in the playlist dropdown onSetMediaObject = mediaObject => { this.viewer.goToPage(this.__getPageNumberForMediaObject(this.context.activeMediaObject)); }; //first load the viewer using the provided this.props.mediaObjects initViewer = () => { //map the media objects to sources OpenSeaDragon likes const sources = this.props.mediaObjects.map(mo => { return this.__toOSDUrl(mo); }); this.viewer = OpenSeadragon({ id: 'img_viewer' , prefixUrl: '/static/node_modules/openseadragon/build/openseadragon/images/', showSelectionControl: true, showRotationControl: true, sequenceMode : true, preserveViewport: true, autoHideControls: false, height: '100px', ajaxWithCredentials : this.props.useCredentials, tileSources: sources.map(s => s.infoUrl), //Note: the initial active media object is determined in ResourceViewer.determineInitialMediaObject() initialPage : this.__getPageNumberForMediaObject(this.context.activeMediaObject) }); //make sure the selection button tooltips have translations (otherwise annoying debug messages) OpenSeadragon.setString('Tooltips.SelectionToggle', 'Toggle selection'); OpenSeadragon.setString('Tooltips.SelectionConfirm', 'Confirm selection'); //add the selection (rectangle) support (Picturae plugin) if(this.props.annotationClient.config.editOptions.Segment) { //TODO change this check later on () this.selector = this.viewer.selection({ showConfirmDenyButtons: true, styleConfirmDenyButtons: true, returnPixelCoordinates: true, //crossOriginPolicy: 'Anonymous', keyboardShortcut: 'c', // key to toggle selection mode rect: null, // initial selection as an OpenSeadragon.SelectionRect object startRotated: false, // alternative method for drawing the selection; useful for rotated crops startRotatedHeight: 0.1, // only used if startRotated=true; value is relative to image height restrictToImage: false, // true = do not allow any part of the selection to be outside the image onSelection: this.onSelection, prefixUrl: '/static/vendor/openseadragonselection-master/images/', navImages: { // overwrites OpenSeadragon's options selection: { REST: 'selection_rest.png', GROUP: 'selection_grouphover.png', HOVER: 'selection_hover.png', DOWN: 'selection_pressed.png' }, selectionConfirm: { REST: 'selection_confirm_rest.png', GROUP: 'selection_confirm_grouphover.png', HOVER: 'selection_confirm_hover.png', DOWN: 'selection_confirm_pressed.png' }, selectionCancel: { REST: 'selection_cancel_rest.png', GROUP: 'selection_cancel_grouphover.png', HOVER: 'selection_cancel_hover.png', DOWN: 'selection_cancel_pressed.png' }, } }); const onPage = async(e) => { const mediaObjectPage = this.__getPageNumberForMediaObject(this.context.activeMediaObject); //only set the media object if navigating via the OSD arrows //when navigating via the PlayListDropDown, the active media object is already set if(e.page !== mediaObjectPage) { await this.context.setActiveMediaObject(this.props.mediaObjects[e.page]); } //FIXME wait for the image to be loaded instead of this ugly timout setTimeout( () => { this.__showSearchHighlight(this.context.activeMediaObject); }, this.OVERLAY_TIMEOUT); // The imageLabelRef hack is due to a bug in OpenSeadragon see: https://github.com/openseadragon/openseadragon/issues/774 //this.props.imageLabelRef.current.style.display = ''; //this.viewer.addOverlay("html-overlay", new OpenSeadragon.Point(0.5, -0.05), OpenSeadragon.Placement.CENTER); }; //make sure the highlights are updated per page/image, annotations are triggered via onChangeTarget this.viewer.addHandler('page', onPage); //Disable zooming on mouse click this.viewer.gestureSettingsMouse.clickToZoom = false; //Add the overlay box //this.viewer.addOverlay("html-overlay", new OpenSeadragon.Point(0.5, -0.05), OpenSeadragon.Placement.CENTER); //return a promise, after the viewer has opened its first image, so an initial overlay can be drawn return new Promise(resolve => { this.viewer.addHandler('open', (target, info) => { this.renderAnnotations(); resolve("viewer loaded"); }); }); //for debugging only /*this.viewer.addHandler('canvas-click', (target, info) => { // The canvas-click event gives us a position in web coordinates. const webPoint = target.position; // Convert that to viewport coordinates, the lingua franca of OpenSeadragon coordinates. const viewportPoint = this.viewer.viewport.pointFromPixel(webPoint); // Convert from viewport coordinates to image coordinates. const imagePoint = this.viewer.viewport.viewportToImageCoordinates(viewportPoint); // Show the results. console.log(webPoint.toString(), viewportPoint.toString(), imagePoint.toString()); });*/ } }; //whenever the annotation target (possibly the viewed image here) has changed onChangeTarget = target => setTimeout( () => {this.renderAnnotations()}, this.OVERLAY_TIMEOUT); onSaveAnnotation = annotation => { this.renderAnnotations(); if(this.selector) this.selector.toggleState(); }; clearAnnotationOverlays = () => { this.annotationOverlays.forEach(overlayId => { this.viewer.removeOverlay(overlayId); }); this.annotationOverlays = []; }; renderAnnotations = () => { this.clearAnnotationOverlays(); if(this.context.activeProject === null) return; //if no project is selected don't draw the annotations const annotations = this.props.annotationClient.annotations || []; annotations.forEach(annotation => { const overlayId = this.renderAnnotationOverlay(annotation); if(overlayId) { this.annotationOverlays.push(overlayId); } }); }; //TODO figure out how to do bind with extra param in ES6 way deleteAnnotation(annotation, e) { if(e) { //FIXME prevent zoom (this solution does not work...) e.preventDefault(); e.stopPropagation(); } this.props.annotationClient.delete(annotation); } onDeleteAnnotation = annotation => { //remove the overlay, once the annotation was deleted from the server this.viewer.removeOverlay(annotation.id); }; /* -------------------------------------------------------------- -------------------------- ANNOTATION CRUD ---------------------- ---------------------------------------------------------------*/ onSelection = async(rect) => { //first set the active selection in the annotation client if(this.context.activeProject === null) return; //you cannot make a segment (annotation) without an active project await this.props.annotationClient.setActiveSelection({ rect : { x : rect.x, y : rect.y, w : rect.width, h : rect.height }, rotation : rect.rotation }); // layer id const activeAnnotation = this.props.annotationClient.activeAnnotation; const layerId = activeAnnotation && activeAnnotation.target && activeAnnotation.target.layerId ? activeAnnotation.target.layerId : 0; this.props.annotationClient.saveSelection(null, false, true, layerId); }; onSetActiveAnnotation = eventData => { this.renderAnnotations(); }; renderAnnotationOverlay = annotation => { //make sure the annotation has a spatial segment in the target if(annotation.target && annotation.target.selector && annotation.target.selector.refinedBy) { const selectedArea = annotation.target.selector.refinedBy.rect; const translatedArea = this.viewer.viewport.imageToViewportRectangle( parseInt(selectedArea.x), parseInt(selectedArea.y), parseInt(selectedArea.w), parseInt(selectedArea.h) ); //the actual overlay div const overlay = document.createElement('div'); const classNames = [ IDUtil.cssClassName('overlay', this.CLASS_PREFIX) ] if(this.props.annotationClient.activeAnnotation && annotation.id == this.props.annotationClient.activeAnnotation.id) { classNames.push('active'); } overlay.className = classNames.join(' '); overlay.onclick= this.props.annotationClient.setActiveAnnotation.bind(this, annotation); overlay.ondblclick = this.props.annotationClient.edit.bind(this, annotation); overlay.id = annotation.id; //add the remove button const removeBtn = document.createElement('button'); removeBtn.onclick = this.deleteAnnotation.bind(this, annotation); const removeIcon = document.createElement('span'); removeIcon.className = IconUtil.getUserActionIcon('remove'); removeBtn.appendChild(removeIcon); overlay.appendChild(removeBtn); this.viewer.addOverlay({ element: overlay, location: translatedArea }); return annotation.id; } return null; } render = () => <div id="img_viewer" className={classNames(IDUtil.cssClassName('image-viewer'),{ multiple: this.props.mediaObjects.length > 1 })}></div>; } ImageViewer.propTypes = { useCredentials: PropTypes.bool, mediaObjects: PropTypes.array, annotationClient: PropTypes.object };