Skip to content
Snippets Groups Projects
Select Git revision
  • b5bb1f87384354a8afd04b078034a74bb7e993c7
  • 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

OpenSeadragonCanvasOverlay.js

Blame
  • OpenSeadragonViewer.js 9.58 KiB
    import React, { Component } from 'react';
    import PropTypes from 'prop-types';
    import isEqual from 'lodash/isEqual';
    import OpenSeadragon from 'openseadragon';
    import ns from '../config/css-ns';
    import OpenSeadragonCanvasOverlay from '../lib/OpenSeadragonCanvasOverlay';
    import CanvasWorld from '../lib/CanvasWorld';
    
    /**
     * Represents a OpenSeadragonViewer in the mirador workspace. Responsible for mounting
     * and rendering OSD.
     */
    export class OpenSeadragonViewer extends Component {
      /**
       * annotationsMatch - compares previous annotations to current to determine
       * whether to add a new updateCanvas method to draw annotations
       * @param  {Array} currentAnnotations
       * @param  {Array} prevAnnotations
       * @return {Boolean}
       */
      static annotationsMatch(currentAnnotations, prevAnnotations) {
        if (currentAnnotations.length === 0 && prevAnnotations.length === 0) return true;
        if (currentAnnotations.length !== prevAnnotations.length) return false;
        return currentAnnotations.every((annotation, index) => {
          const newIds = annotation.resources.map(r => r.id);
          const prevIds = prevAnnotations[index].resources.map(r => r.id);
          if (newIds.length === 0 && prevIds.length === 0) return true;
          if (newIds.length !== prevIds.length) return false;
    
          if ((annotation.id === prevAnnotations[index].id) && (isEqual(newIds, prevIds))) {
            return true;
          }
          return false;
        });
      }
    
      /**
       *
       * @param e
       */
      static onCanvasClick(e) {
        e.preventDefaultAction = true;
      }
    
      /**
       * @param {Object} props
       */
      constructor(props) {
        super(props);
    
        this.viewer = null;
        this.osdCanvasOverlay = null;
        // An initial value for the updateCanvas method
        this.updateCanvas = () => {};
        this.ref = React.createRef();
        this.onCanvasFocus = this.onCanvasFocus.bind(this);
        this.onUpdateViewport = this.onUpdateViewport.bind(this);
        this.onViewportChange = this.onViewportChange.bind(this);
        this.zoomToWorld = this.zoomToWorld.bind(this);
        this.osdUpdating = false;
        this.justReceivedFocus = false;
      }
    
      /**
       * React lifecycle event
       */
      componentDidMount() {
        const {
          config,
          viewer,
        } = this.props;
        if (!this.ref.current) {
          return;
        }
    
        this.viewer = new OpenSeadragon({
          alwaysBlend: false,
          blendTime: 0.1,
          id: this.ref.current.id,
          preserveImageSizeOnResize: true,
          preserveViewport: true,
          showNavigationControl: false,
        });
    
        this.osdCanvasOverlay = new OpenSeadragonCanvasOverlay(this.viewer);
        this.viewer.addHandler('update-viewport', this.onUpdateViewport);
        // Set a flag when OSD starts animating (so that viewer updates are not used)
        this.viewer.addHandler('animation-start', () => {
          this.osdUpdating = true;
        });
        this.viewer.addHandler('animation-finish', this.onViewportChange);
        this.viewer.addHandler('animation-finish', () => {
          this.osdUpdating = false;
        });
        if (config && config.isCanvasClickZoomActionDisabled) {
          this.viewer.addHandler('canvas-click', OpenSeadragonViewer.onCanvasClick);
        } else if (config && config.isCanvasClickZoomActionAfterFocusEnabled) {
          this.viewer.addHandler('canvas-click', this.onCanvasFocus);
        }
        if (viewer) {
          this.viewer.viewport.panTo(viewer, true);
          this.viewer.viewport.zoomTo(viewer.zoom, viewer, true);
        }
        this.addAllTileSources();
      }
    
      /**
       * 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) {
        const {
          focused, highlightedAnnotations, selectedAnnotations, viewer,
        } = this.props;
        const highlightsUpdated = !OpenSeadragonViewer.annotationsMatch(
          highlightedAnnotations, prevProps.highlightedAnnotations,
        );
        const selectionsUpdated = !OpenSeadragonViewer.annotationsMatch(
          selectedAnnotations, prevProps.selectedAnnotations,
        );
    
        if (highlightsUpdated || selectionsUpdated) {
          this.updateCanvas = () => {
            this.osdCanvasOverlay.clear();
            this.osdCanvasOverlay.resize();
            this.osdCanvasOverlay.canvasUpdate(() => {
              this.annotationsToContext(highlightedAnnotations, '#00BFFF');
              this.annotationsToContext(selectedAnnotations, 'yellow');
            });
          };
          this.viewer.forceRedraw();
        }
    
        if (focused && prevProps.focused !== focused) {
          this.justReceivedFocus = true;
          setTimeout(() => { this.justReceivedFocus = false; }, 50);
        }
    
        if (!this.tileSourcesMatch(prevProps.tileSources)) {
          this.viewer.close();
          this.addAllTileSources();
        } else if (viewer && !this.osdUpdating) {
          const { viewport } = this.viewer;
    
          if (viewer.x !== viewport.centerSpringX.target.value
            || viewer.y !== viewport.centerSpringY.target.value) {
            this.viewer.viewport.panTo(viewer, false);
          }
    
          if (viewer.zoom !== viewport.zoomSpring.target.value) {
            this.viewer.viewport.zoomTo(viewer.zoom, viewer, false);
          }
        }
      }
    
      /**
       */
      componentWillUnmount() {
        this.viewer.removeAllHandlers();
      }
    
      /**
       *
       * @param e
       */
      onCanvasFocus(e) {
        e.preventDefaultAction = this.justReceivedFocus;
        this.justReceivedFocus = false;
      }
    
      /**
       * onUpdateViewport - fires during OpenSeadragon render method.
       */
      onUpdateViewport(event) {
        this.updateCanvas();
      }
    
      /**
       * Forward OSD state to redux
       */
      onViewportChange(event) {
        const { updateViewport, windowId } = this.props;
    
        const { viewport } = event.eventSource;
    
        updateViewport(windowId, {
          x: Math.round(viewport.centerSpringX.target.value),
          y: Math.round(viewport.centerSpringY.target.value),
          zoom: viewport.zoomSpring.target.value,
        });
      }
    
      /**
       * annotationsToContext - converts anontations to a canvas context
       */
      annotationsToContext(annotations, color = 'yellow') {
        const { canvasWorld } = this.props;
        const context = this.osdCanvasOverlay.context2d;
        const zoom = this.viewer.viewport.getZoom(true);
        const width = canvasWorld.worldBounds()[2];
        annotations.forEach((annotation) => {
          annotation.resources.forEach((resource) => {
            const offset = canvasWorld.offsetByCanvas(resource.targetId);
            const fragment = resource.fragmentSelector;
            fragment[0] += offset.x;
            context.strokeStyle = color;
            context.lineWidth = Math.ceil(10 / (zoom * width));
            context.strokeRect(...fragment);
          });
        });
      }
    
      /** */
      addAllTileSources() {
        const { tileSources } = this.props;
        Promise.all(
          tileSources.map((tileSource, i) => this.addTileSource(tileSource, i)),
        ).then(() => {
          if (tileSources[0]) {
            this.zoomToWorld();
          }
        });
      }
    
      /**
       */
      addTileSource(tileSource, i = 0) {
        const { canvasWorld } = this.props;
        return new Promise((resolve, reject) => {
          if (!this.viewer) {
            return;
          }
          this.viewer.addTiledImage({
            error: event => reject(event),
            fitBounds: new OpenSeadragon.Rect(
              ...canvasWorld.canvasToWorldCoordinates(i),
            ),
            success: event => resolve(event),
            tileSource,
          });
        });
      }
    
      /**
       */
      fitBounds(x, y, w, h, immediately = true) {
        this.viewer.viewport.fitBounds(
          new OpenSeadragon.Rect(x, y, w, h),
          immediately,
        );
      }
    
      /**
       * tileSourcesMatch - compares previous tileSources to current to determine
       * whether a refresh of the OSD viewer is needed.
       * @param  {Array} prevTileSources
       * @return {Boolean}
       */
      tileSourcesMatch(prevTileSources) {
        const { tileSources } = this.props;
        if (tileSources.length === 0 && prevTileSources.length === 0) return true;
    
        return tileSources.some((tileSource, index) => {
          if (!prevTileSources[index]) {
            return false;
          }
          if (tileSource['@id'] === prevTileSources[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 {
          windowId, children, label, t,
        } = this.props;
    
        const enhancedChildren = React.Children.map(children, child => (
          React.cloneElement(
            child,
            {
              zoomToWorld: this.zoomToWorld,
            },
          )
        ));
    
        return (
          <>
            <section
              className={ns('osd-container')}
              id={`${windowId}-osd`}
              ref={this.ref}
              aria-label={t('item', { label })}
            >
              { enhancedChildren }
            </section>
          </>
        );
      }
    }
    
    OpenSeadragonViewer.defaultProps = {
      children: null,
      config: null,
      focused: false,
      highlightedAnnotations: [],
      label: null,
      selectedAnnotations: [],
      tileSources: [],
      viewer: null,
    };
    
    OpenSeadragonViewer.propTypes = {
      canvasWorld: PropTypes.instanceOf(CanvasWorld).isRequired,
      children: PropTypes.node,
      config: PropTypes.object, // eslint-disable-line react/forbid-prop-types
      focused: PropTypes.bool,
      highlightedAnnotations: PropTypes.arrayOf(PropTypes.object),
      label: PropTypes.string,
      selectedAnnotations: PropTypes.arrayOf(PropTypes.object),
      t: PropTypes.func.isRequired,
      tileSources: PropTypes.arrayOf(PropTypes.object),
      updateViewport: PropTypes.func.isRequired,
      viewer: PropTypes.object, // eslint-disable-line react/forbid-prop-types
      windowId: PropTypes.string.isRequired,
    };