labo-components
Version:
334 lines (282 loc) • 13.3 kB
JSX
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
};