Skip to content
Snippets Groups Projects
Select Git revision
  • 338fb8b695421bbe1264dc1265b7060e2c2a0957
  • annotation-on-video default protected
  • demo_ci
  • 3-upstream-01022023
  • master
  • gh3538-captions
  • 16-adapt-for-images-annot
  • 15-api-for-annotations-on-video
  • 15-annotations-on-videos
  • video_for_annotations
  • wip-1-annotations-on-videos
  • 9-videoviewer-tests
  • 9_wip_videotests
  • 6-fix-tests-and-ci
  • _fix_ci
  • wip-webpack-from-git
16 results

OpenSeadragonViewer.js

Blame
  • user avatar
    Chris Beer authored and GitHub committed
    338fb8b6
    History
    OpenSeadragonViewer.js 11.49 KiB
    import React, { Component } from 'react';
    import PropTypes from 'prop-types';
    import debounce from 'lodash/debounce';
    import isEqual from 'lodash/isEqual';
    import OpenSeadragon from 'openseadragon';
    import classNames from 'classnames';
    import ns from '../config/css-ns';
    import AnnotationsOverlay from '../containers/AnnotationsOverlay';
    import CanvasWorld from '../lib/CanvasWorld';
    import { PluginHook } from './PluginHook';
    import { OSDReferences } from '../plugins/OSDReferences';
    
    /**
     * Represents a OpenSeadragonViewer in the mirador workspace. Responsible for mounting
     * and rendering OSD.
     */
    export class OpenSeadragonViewer extends Component {
      /**
       * @param {Object} props
       */
      constructor(props) {
        super(props);
    
        this.state = { viewer: undefined };
        this.ref = React.createRef();
        this.apiRef = React.createRef();
        OSDReferences.set(props.windowId, this.apiRef);
        this.onCanvasMouseMove = debounce(this.onCanvasMouseMove.bind(this), 10);
        this.onViewportChange = this.onViewportChange.bind(this);
        this.zoomToWorld = this.zoomToWorld.bind(this);
        this.osdUpdating = false;
      }
    
      /**
       * React lifecycle event
       */
      componentDidMount() {
        const { osdConfig, t, windowId } = this.props;
        if (!this.ref.current) {
          return;
        }
    
        const viewer = new OpenSeadragon({
          id: this.ref.current.id,
          ...osdConfig,
        });
    
        const canvas = viewer.canvas && viewer.canvas.firstElementChild;
        if (canvas) {
          canvas.setAttribute('role', 'img');
          canvas.setAttribute('aria-label', t('digitizedView'));
          canvas.setAttribute('aria-describedby', `${windowId}-osd`);
        }
    
        this.apiRef.current = viewer;
    
        this.setState({ viewer });
    
        // Set a flag when OSD starts animating (so that viewer updates are not used)
        viewer.addHandler('animation-start', () => {
          this.osdUpdating = true;
        });
        viewer.addHandler('animation-finish', this.onViewportChange);
        viewer.addHandler('animation-finish', () => {
          this.osdUpdating = false;
        });
    
        if (viewer.innerTracker) {
          viewer.innerTracker.moveHandler = this.onCanvasMouseMove;
        }
      }
    
      /**
       * When the tileSources change, make sure to close the OSD viewer.
       * When the annotations change, reset the updateCanvas method to make sure
       * they are added.
       * When the viewport state changes, pan or zoom the OSD viewer as appropriate
       */
      componentDidUpdate(prevProps, prevState) {
        const {
          viewerConfig,
          canvasWorld,
        } = this.props;
        const { viewer } = this.state;
        this.apiRef.current = viewer;
    
        if (prevState.viewer === undefined) {
          if (viewerConfig) {
            viewer.viewport.panTo(viewerConfig, true);
            viewer.viewport.zoomTo(viewerConfig.zoom, viewerConfig, true);
            viewerConfig.degrees !== undefined && viewer.viewport.setRotation(viewerConfig.degrees);
            viewerConfig.flip !== undefined && viewer.viewport.setFlip(viewerConfig.flip);
          }
    
          this.addAllImageSources(!(viewerConfig));
    
          return;
        }
    
        if (!this.infoResponsesMatch(prevProps.infoResponses)
          || !this.nonTiledImagedMatch(prevProps.nonTiledImages)
        ) {
          viewer.close();
          const canvasesChanged = !(isEqual(canvasWorld.canvasIds, prevProps.canvasWorld.canvasIds));
          this.addAllImageSources((canvasesChanged || !viewerConfig));
        } else if (!isEqual(canvasWorld.layers, prevProps.canvasWorld.layers)) {
          this.refreshTileProperties();
        } else if (viewerConfig && !this.osdUpdating) {
          const { viewport } = viewer;
    
          if (viewerConfig.x !== viewport.centerSpringX.target.value
            || viewerConfig.y !== viewport.centerSpringY.target.value) {
            viewport.panTo(viewerConfig, false);
          }
    
          if (viewerConfig.zoom !== viewport.zoomSpring.target.value) {
            viewport.zoomTo(viewerConfig.zoom, viewerConfig, false);
          }
    
          if (viewerConfig.rotation !== viewport.getRotation()) {
            viewport.setRotation(viewerConfig.rotation);
          }
    
          if (viewerConfig.flip !== viewport.getFlip()) {
            viewport.setFlip(viewerConfig.flip);
          }
        }
      }
    
      /**
       */
      componentWillUnmount() {
        const { viewer } = this.state;
    
        if (viewer.innerTracker
          && viewer.innerTracker.moveHandler === this.onCanvasMouseMove) {
          viewer.innerTracker.moveHandler = null;
        }
        viewer.removeAllHandlers();
        this.apiRef.current = undefined;
      }
    
      /** Shim to provide a mouse-move event coming from the viewer */
      onCanvasMouseMove(event) {
        const { viewer } = this.state;
    
        viewer.raiseEvent('mouse-move', event);
      }
    
      /**
       * Forward OSD state to redux
       */
      onViewportChange(event) {
        const { updateViewport, windowId } = this.props;
    
        const { viewport } = event.eventSource;
    
        updateViewport(windowId, {
          flip: viewport.getFlip(),
          rotation: viewport.getRotation(),
          x: Math.round(viewport.centerSpringX.target.value),
          y: Math.round(viewport.centerSpringY.target.value),
          zoom: viewport.zoomSpring.target.value,
        });
      }
    
      /** */
      addAllImageSources(zoomAfterAdd = true) {
        const { nonTiledImages, infoResponses } = this.props;
        Promise.all(
          infoResponses.map(infoResponse => this.addTileSource(infoResponse)),
          nonTiledImages.map(image => this.addNonTiledImage(image)),
        ).then(() => {
          if (infoResponses[0] || nonTiledImages[0]) {
            if (zoomAfterAdd) this.zoomToWorld();
            this.refreshTileProperties();
          }
        });
      }
    
      /** */
      addNonTiledImage(contentResource) {
        const { canvasWorld } = this.props;
        const { viewer } = this.state;
    
        const type = contentResource.getProperty('type');
        const format = contentResource.getProperty('format') || '';
    
        if (!(type === 'Image' || type === 'dctypes:Image' || format.startsWith('image/'))) return Promise.resolve();
    
        return new Promise((resolve, reject) => {
          if (!viewer) {
            reject();
          }
    
          viewer.addSimpleImage({
            error: event => reject(event),
            fitBounds: new OpenSeadragon.Rect(
              ...canvasWorld.contentResourceToWorldCoordinates(contentResource),
            ),
            index: canvasWorld.layerIndexOfImageResource(contentResource),
            opacity: canvasWorld.layerOpacityOfImageResource(contentResource),
            success: event => resolve(event),
            url: contentResource.id,
          });
        });
      }
    
      /**
       */
      addTileSource(infoResponse) {
        const { canvasWorld } = this.props;
        const { viewer } = this.state;
        return new Promise((resolve, reject) => {
          if (!viewer) {
            reject();
          }
    
          // OSD mutates this object, so we give it a shallow copy
          const tileSource = { ...infoResponse.json };
          const contentResource = canvasWorld.contentResource(infoResponse.id);
    
          if (!contentResource) return;
    
          viewer.addTiledImage({
            error: event => reject(event),
            fitBounds: new OpenSeadragon.Rect(
              ...canvasWorld.contentResourceToWorldCoordinates(contentResource),
            ),
            index: canvasWorld.layerIndexOfImageResource(contentResource),
            opacity: canvasWorld.layerOpacityOfImageResource(contentResource),
            success: event => resolve(event),
            tileSource,
          });
        });
      }
    
      /** */
      refreshTileProperties() {
        const { canvasWorld } = this.props;
        const { viewer: { world } } = this.state;
    
        const items = [];
        for (let i = 0; i < world.getItemCount(); i += 1) {
          items.push(world.getItemAt(i));
        }
    
        items.forEach((item, i) => {
          const contentResource = canvasWorld.contentResource(item.source['@id'] || item.source.id);
          if (!contentResource) return;
          const newIndex = canvasWorld.layerIndexOfImageResource(contentResource);
          if (i !== newIndex) world.setItemIndex(item, newIndex);
          item.setOpacity(canvasWorld.layerOpacityOfImageResource(contentResource));
        });
      }
    
      /**
       */
      fitBounds(x, y, w, h, immediately = true) {
        const { viewer } = this.state;
    
        viewer.viewport.fitBounds(
          new OpenSeadragon.Rect(x, y, w, h),
          immediately,
        );
      }
    
      /**
       * infoResponsesMatch - compares previous tileSources to current to determine
       * whether a refresh of the OSD viewer is needed.
       * @param  {Array} prevTileSources
       * @return {Boolean}
       */
      infoResponsesMatch(prevInfoResponses) {
        const { infoResponses } = this.props;
    
        if (infoResponses.length === 0 && prevInfoResponses.length === 0) return true;
        if (infoResponses.length !== prevInfoResponses.length) return false;
    
        return infoResponses.every((infoResponse, index) => {
          if (!prevInfoResponses[index]) {
            return false;
          }
    
          if (!infoResponse.json || !prevInfoResponses[index].json) {
            return false;
          }
    
          if (infoResponse.tokenServiceId !== prevInfoResponses[index].tokenServiceId) {
            return false;
          }
    
          if (infoResponse.json['@id']
            && infoResponse.json['@id'] === prevInfoResponses[index].json['@id']) {
            return true;
          }
          if (infoResponse.json.id
            && infoResponse.json.id === prevInfoResponses[index].json.id) {
            return true;
          }
    
          return false;
        });
      }
    
      /**
       * nonTiledImagedMatch - compares previous images to current to determin
       * whether a refresh of the OSD viewer is needed
       */
      nonTiledImagedMatch(prevNonTiledImages) {
        const { nonTiledImages } = this.props;
        if (nonTiledImages.length === 0 && prevNonTiledImages.length === 0) return true;
    
        return nonTiledImages.some((image, index) => {
          if (!prevNonTiledImages[index]) {
            return false;
          }
          if (image.id === prevNonTiledImages[index].id) {
            return true;
          }
          return false;
        });
      }
    
      /**
       * zoomToWorld - zooms the viewer to the extent of the canvas world
       */
      zoomToWorld(immediately = true) {
        const { canvasWorld } = this.props;
        this.fitBounds(...canvasWorld.worldBounds(), immediately);
      }
    
      /**
       * Renders things
       */
      render() {
        const {
          children, classes, label, t, windowId,
          drawAnnotations,
        } = this.props;
        const { viewer } = this.state;
    
        const enhancedChildren = React.Children.map(children, child => (
          React.cloneElement(
            child,
            {
              zoomToWorld: this.zoomToWorld,
            },
          )
        ));
    
        return (
          <>
            <section
              className={classNames(ns('osd-container'), classes.osdContainer)}
              id={`${windowId}-osd`}
              ref={this.ref}
              aria-label={t('item', { label })}
              aria-live="polite"
            >
              { drawAnnotations
                && <AnnotationsOverlay viewer={viewer} windowId={windowId} /> }
              { enhancedChildren }
              <PluginHook viewer={viewer} {...{ ...this.props, children: null }} />
            </section>
          </>
        );
      }
    }
    
    OpenSeadragonViewer.defaultProps = {
      children: null,
      drawAnnotations: false,
      infoResponses: [],
      label: null,
      nonTiledImages: [],
      osdConfig: {},
      viewerConfig: null,
    };
    
    OpenSeadragonViewer.propTypes = {
      canvasWorld: PropTypes.instanceOf(CanvasWorld).isRequired,
      children: PropTypes.node,
      classes: PropTypes.objectOf(PropTypes.string).isRequired,
      drawAnnotations: PropTypes.bool,
      infoResponses: PropTypes.arrayOf(PropTypes.object), // eslint-disable-line react/forbid-prop-types
      label: PropTypes.string,
      nonTiledImages: PropTypes.array, // eslint-disable-line react/forbid-prop-types
      osdConfig: PropTypes.object, // eslint-disable-line react/forbid-prop-types
      t: PropTypes.func.isRequired,
      updateViewport: PropTypes.func.isRequired,
      viewerConfig: PropTypes.object, // eslint-disable-line react/forbid-prop-types
      windowId: PropTypes.string.isRequired,
    };